Skip to content

RFC: consistent icon system across apps and server #16

Description

@susnux

Current state

Historically we have CSS class based icons, but they suffer one major issue:
They are hard to theme, meaning for dark mode or even for users primary color.

So to overcome this issue we switched to inline SVGs for icons, as they can be themed just with CSS and adjust automatically to the current theme.

Problems

The first problem we at least had in the past is that we have inconsistent design across apps, because some used icons provided by server (css classes), some Material Design Icons and some already Material Symbols.

The bigger problem is when we want to change the icon set used, like at the time of writing the long term goal in design is to move from MDI to Material Symbols. Changing icons need every app, every component and potentially API to be adjusted to use a different library with potentially different names.

Also implementation wise this currently leads to various approaches which are often mixed:

  • Use vue-material-design-icons -> Vue components of SVGs
  • Use @mdi/svg -> use of inline SVG files which are then rendered by e.g. NcIconSvgWrapper
  • Use @mdi/js -> inline SVG but we try to circumvent the problem of bigger bundle size due to inline SVGs by only using the path of the SVG.
  • Use legacy icon classes provided by server (e.g. also used by some API like the contact menu)

To sum up:

  • Inconsistent icon design
  • Hard to switch to different icons
    • also means no customization possible (branding)
  • Increased app bundle sizes due to inline SVG graphics into code
  • SVGs cannot be returned in API as this bloats the requests when a common icon is used
  • Code complexity to support all those ways

Solutions

Seeing those problems and also the initial problem with icon classes I came up with this two options that enhance the current situation:

Unify to SVG and provide a common @nextcloud/icons SVG package

  • Pro:
    • allows to change the icons based on a version of the package
    • allows to return common identifier from API that resolves to an SVG icon controlled by us
    • only one way to apply icons
  • Con:
    • still no customization (branding)
    • still bigger app bundles

Unify to server provided SVG icons
Similar to the previous idea this one is a bit different, here we do not provide a package with those SVGs but instead we use a modern approach and let the server provide an icon sprite that is used by SVGs of the apps.

  • Pro:
    • allows to change the icons based on the server version
    • theoretically even allows to change the icon sprite by admin (branding)
    • allows to return common identifier from API that resolves to an SVG icon controlled by us
    • apps no longer need to bundle the icons thus slightly smaller bundles and faster JS parsing
  • Con:
    • first time loading the icon sprite is another (but cached!) HTTP request

Proposed solution

We move icons into server as mentioned above - this way the server is the only source of truth for icon design.
Meaning apps no longer need to care about package versions or design changes.

With SVG <symbol> it is possible to group multiple icons into one SVG file which looks like:

<svg>
    <symbol id="arrow-up">
        <!-- the content of that icon -->
    </symbol>
    <!-- more icons -->
</svg>

This file can easily be generated by already existing tools.

Then in your HTML you can refer the icon you like with:

<svg style="width: 20px; height: 20px; fill: currentColor;">
  <use href="/core/img/icons.svg#arrow-up" />
</svg>

The benefit here is that any migration would be quite easy:

  • API expects an SVG? Just pass the icon svg as show above - could be done with a shared method.
  • API only provides a icon class? No problem we can now keep that API and just use the icon IDs where WE controll the name
  • We can easily create a component for this

To sum up this would - from my point of view - make working with icons much easier:

  • we have all benefits of inline SVGs
  • we have the benefit of small API responses (icon names only)
  • we can use branding
  • we can change design based on server - so apps will always look good even if they support multiple versions

Further reference

Activity

  1. skjnldsv commented on Jul 23, 2025

    @skjnldsv
    Member

    Ahaha I had the exact same idea a few years ago. Wanted to unify the way we load them via svg only.
    Edit: I mean, not the exact same idea, but the same dilemma :)

    The issue we had was that android used to handle svg very poorly and iOS was...not working at all.

    So in the end, the web dev stack was already working well without the need to unify as we had the svg icon wrapper and the mdi/vue-material-icon.
    I personally don't see the benefit of merging everything into server. This caused too much issues in the past and reduced our flexibility too much.

    I've yet to face an issue with the apps/server parity since we started using the inline svgs.

  2. ShGKme commented on Jul 23, 2025

    @ShGKme

    We move icons into server as mentioned above - this way the server is the only source of truth for icon design

    How is the set of icons defined?
    Do we bundle all existing material design icons?

  3. susnux commented on Jul 23, 2025

    @susnux
    Author

    Do we bundle all existing material design icons?

    Would be only Material Symbols probably ;)

    How is the set of icons defined?

    This would be up to decide which icons are useful.
    Currently all apps we maintain use 380 different icons from MDI (maybe ~45 duplicates due to the recent outline changes), but even if we include 400 then load wise we would have below 200 kiB uncompressed SVG (could be down to 50 kiB compressed) cached by the browser.

    icons.txt

  4. ShGKme commented on Jul 23, 2025

    @ShGKme

    There are ~7.5 thousand icons in the @mdi/js.
    If we bundle all of them, we have 2.5MB only of the path without wrappers.
    While the average size of a single icon is just 345b.

    Bundling everything doesn't make sense to me. An average app uses a small amount of it.

  5. ShGKme commented on Jul 23, 2025

    @ShGKme

    This would be up to decide which icons are useful.

    And if another day we have a new UI element in a specific app - we need to update the server? Or we do it in runtime on the server side?

    And then we need to make sure it still works on a different server version.

  6. AndyScherzinger commented on Jul 23, 2025

    @AndyScherzinger
    Member

    How is the set of icons defined?

    Like susnux said and that would also match @jancborchardt's statement when we discussed icons in terms of "which providing party", to go with Material Symbols meaning "Only", so the hypothesis is that we always find a icon that works and should not use any other lib for an icon.

    Also with some customers in mind the solution shall allow for replacing the whole set given that the large white-label installations replace the icons and will keep doing so. Hence making this easy or easier than today would be beneficial.

    This would also solve: #16 (comment)

  7. susnux commented on Jul 23, 2025

    @susnux
    Author

    There are ~7.5 thousand icons in the @mdi/js.

    Yes but mostly duplicates - there are exactly 3732 Material Symbols.
    Even if we bundle all together we end up with ~1.45 MiB uncompressed - this is quite similar to icon fonts used in other projects - but still allows for having svg benefits.

    Bundling everything doesn't make sense to me. An average app uses a small amount of it.

    Sure thats why this would only be provided by server - so all apps share it without any need to bundle that into the app.
    Also main reason I considered this is caching of it - it basically can stay for a very long time in cache without need for requests and without the need for implementing this as its native browser behavior.

  8. ShGKme commented on Jul 23, 2025

    @ShGKme

    Like susnux said and that would also match @jancborchardt's statement when we discussed icons in terms of "which providing party", to go with Material Symbols meaning "Only", so the hypothesis is that we always find a icon that works and should not use any other lib for an icon.

    Limiting the icons set is possible with both approaches:

    1. Library (can be a large set, everybody uses what they need)
    2. Sprite (we need to provide unused icons as well)

    @jancborchardt proposal wasn't about a small subset of icons.
    Only Material Symbols is much more than "only a specific subset of icons we currently use in some apps".

  9. ShGKme commented on Jul 23, 2025

    @ShGKme

    Currently all apps we maintain use 380 different icons from MDI

    there are exactly 3732 Material Symbols.
    Even if we bundle all together we end up with ~1.45 MiB uncompressed

    So, is the proposal about bundling all the symbols or a very limited subset? 380 is about 10% of Material Symbols.

  10. Antreesy commented on Jul 23, 2025

    @Antreesy

    IMO it feels like the library with tree-shaking and options like outline/filled and path/vue-component would fit most of cases.

    Not strictly against server approach, just exploring: does it sound doable to fork https://github.com/marella/material-symbols or use similar approach with build script and automation, to get the dist we needed?

  11. skjnldsv commented on Jul 23, 2025

    @skjnldsv
    Member

    While the idea is good if you look at the outcome, the process of gathering those icons in an efficient and flexible way between all apps seems impossible to me. We'll just end up facing the same issues as we did before when apps had to have some of their icons in server and it made the entire release very complex.

    The only solution I could see is dynamically loading/caching svg somehow with a declarative approach (apps state what they need, server bundles them), which could be cached for each patch release.
    But this brings its entire set of issues

  12. skjnldsv commented on Jul 23, 2025

    @skjnldsv
    Member

    IMO it feels like the library with tree-shaking and options like outline/filled and path/vue-component would fit most of cases.

    I think the curren solution using @mdi and the Vue lib is already close enough. Creating another Nextcloud library that would be basically a wrapper would just complicate things for minimal gain.
    And I'm not even talking about the fact that we'll again have 2-3 people maintaining that lib which adds to the existing crazy stack of maintained ones 🥲

  13. Antreesy commented on Jul 23, 2025

    @Antreesy

    using @mdi/js and the Vue lib is already close enough

    Depends on the future goal we set.
    Both are missing material symbols.
    And both already don't cover full set of available material icons, I have to manually integrate some for an app. Additionally, we are bound to their releases, where last one was 2y ago.

  14. susnux commented on Jul 23, 2025

    @susnux
    Author

    So, is the proposal about bundling all the symbols or a very limited subset? 380 is about 10% of Material Symbols.

    I did not think about which icons - in a first place it was about the general idea of this to resolve the mentioned issues.
    For flexibility its probably would be good to bundle all Material Symbols.

    using @mdi/js and the Vue lib is already close enough

    No because it does not provide Material Symbols - on the long term the design goal for us is to switch finally from old MDI to Material Symbols. Also both have all the limitations mentioned above.

    And I'm not even talking about the fact that we'll again have 2-3 people maintaining that lib which adds to the existing crazy stack of maintained ones 🥲

    Yes I fully agree! Thats also why I prefer the server provided sprite solution - no extra maintenance needed.

    IMO it feels like the library with tree-shaking and options like outline/filled and path/vue-component would fit most of cases.

    Yes as pointed out in the post it works at least partly. We can introduce consistent IDs and thus fix the API issue with have currently with MDI (e.g. if we in the future move away from material to something different we only need to change the paths).

    But I personally do not see the benefit of having such library compared to the sprite approach.
    Because from the developer perspective both would be transparent, meaning you would in both cases not care how it works.
    Same time you still would keep the issues with inconsistency if your app targets multiple Nextcloud versions at the same time and it does not allow branding.

    Having such a library was also my first idea a year ago when I first thought about this and had a chat with Jan about switching to Material Symbols. But after thinking about it for quite some time I think the sprite solution is superior.


    So why I think the sprite solution is nice, is that it solves all our current problem:

    • Use Material Symbols instead of MDI
    • Consistent look across apps as icons are bound to server version and not compile-time library version
    • Static IDs we control - so if we choose a different icon set someday we just need to replace the icons but keep their names
    • Thus with the static IDs we can use the ID in PHP API responses - no URL needed
    • Backwards compatibility as its also just SVG so every API (like files) that expects a SVG still works fine

    There is only one downside:
    Initial load is increased by the icon bundle size (2MiB in case of Material Symbols).
    Of course webserver compresses it so we transfer 500kiB but still this is 10 times more than with inline SVGs.
    But this file is then cached by the browser so we only talk about the first load.

    And we can even improve this by adding this to the HTML template:
    <link rel="preload" as="image" href="/core/img/icons.svg">
    This way its preloaded when the browser has some rest and we do not see loading in the UI.

    So if you want to try, then I created a sprite for you attached.
    For example put this in /core/img then you can try it by adding this to Nextcloud:

    <svg fill="currentColor" width="20" height="20">
      <use href="/core/img/icons.svg#celebration"></use>
    </svg>

    Implementation wise I imagine two things:

    1. A method to generate an SVG string for use with current inline-SVG APIs¹
    2. Add it to NcIconSvgWrapper (or add NcIcon)

    So in Vue you could do <NcIcon name="celebration" /> / <NcIconSvgWrapper icon="celebration" /> and e.g. when you want to pass a SVG to for example the files action API all you need is to call getSvgIcon('celebration'). Meaning no longer any need to import anything :)

  15. ChristophWurst commented on Jul 24, 2025

    @ChristophWurst
    Member

    Unify to server provided SVG icons
    Similar to the previous idea this one is a bit different, here we do not provide a package with those SVGs but instead we use a modern approach and let the server provide an icon sprite that is used by SVGs of the apps.

    to add to the cons: apps will be less independent/standalone again in terms of web frontend. If there's a new icon an app wants to use it becomes version dependent. This is not an issue for apps that go in lockstep with the server releases, but potentially troublesome for those with wide support ranges.

  16. skjnldsv commented on Jul 24, 2025

    @skjnldsv
    Member

    So, I would be ok with the sprite approach if and only if server provides the full lib (or a wide subset of it at least).
    So that other apps doesn't have to worry about server not having the icon they wanna use.
    If apps want to use their own icons, they are free to keep using inline svg anyway.

    Regarding the 2MB load, if we remove all the inline svg apps load on nextcloud I wonder how much that is already.
    I wouldn't be surprised we reach hundreds of KB already.

    The sprite would indeed remove the need for those duplicates

  17. skjnldsv commented on Jul 24, 2025

    @skjnldsv
    Member

    If we serve as svgz, it's down to 786K 545K
    EDIT: script was wrong, it's actually smaller

    [admin@workstation] $ svg_sprite_script.sh .
    Processing: 10k
    Processing: 10mp
    ...
    Processing: zoom_out_map
    Processing: zoom_out
    Created icons.svg with 3657 icons
    
    [admin@workstation] $ ls icons.svg.gz
    545K│icons.svg.gz
    Script
    #!/bin/zsh
    
    # Check if input directory argument is provided
    if [[ $# -eq 0 ]]; then
        echo "Usage: $0 <input-directory> [output-file]"
        echo "Example: $0 ./svg-icons icons.svg"
        exit 1
    fi
    
    # Configuration
    INPUT_DIR="$1"
    OUTPUT_FILE="${2:-icons.svg}"  # Use second argument or default to icons.svg
    
    # Check if input directory exists
    if [[ ! -d "$INPUT_DIR" ]]; then
        echo "Error: Directory $INPUT_DIR does not exist"
        exit 1
    fi
    
    # Start creating the sprite file
    echo '<?xml version="1.0" encoding="UTF-8"?>' > "$OUTPUT_FILE"
    echo '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none;">' >> "$OUTPUT_FILE"
    
    # Counter for processed files
    count=0
    
    # Process each SVG file in the directory
    for svg_file in "$INPUT_DIR"/*.svg; do
        # Skip if no SVG files found
        [[ ! -f "$svg_file" ]] && continue
        
        # Skip the output file if it's in the same directory
        [[ "$(basename "$svg_file")" == "$(basename "$OUTPUT_FILE")" ]] && continue
        
        # Get filename without extension for the ID
        filename=$(basename "$svg_file" .svg)
        
        echo "Processing: $filename"
        
        # Extract only the content inside <svg> tags and wrap in <symbol>
        echo "  <symbol id=\"$filename\">" >> "$OUTPUT_FILE"
        
        # Extract everything between <svg> and </svg> tags, excluding the tags themselves
        sed -n '/<svg/,/<\/svg>/p' "$svg_file" | \
        sed '1s/.*<svg[^>]*>//' | \
        sed '$s/<\/svg>.*//' | \
        sed '/^$/d' >> "$OUTPUT_FILE"
        
        echo "  </symbol>" >> "$OUTPUT_FILE"
        
        ((count++))
    done
    
    # Close the sprite SVG
    echo '</svg>' >> "$OUTPUT_FILE"
    
    echo "Created $OUTPUT_FILE with $count icons"
  18. AndyScherzinger commented on Jul 24, 2025

    @AndyScherzinger
    Member

    So, I would be ok with the sprite approach if and only if server provides the full lib (or a wide subset of it at least).

    I also think for simplicity of maintaining the offer of an icon set that this is the way to go. Also with the "We shouldn't need an icon not covered by Material Symbols". So I'd say fine with this set and devs still have the flexibility to ship own icons if needed which like said, shouldn't be the case. App icon is an exception of course.

  19. jancborchardt commented on Jul 24, 2025

    @jancborchardt
    Member

    Also with the "We shouldn't need an icon not covered by Material Symbols".

    Yep exactly. This limitation will help standardization and prevent proliferation of using icons which are difficult to understand.

  20. susnux commented on Feb 28, 2026

    @susnux
    Author

    There were no negative opinions about this here 🎉
    So I had some time to create a demo for this feature:

    I created some scripts to generate the sprite map from the Material Symbols (as well as some metadata for documentation - see further below):

    https://github.com/susnux/nextcloud-icons

    This is probably not that interesting, but you can see that we can maintain a list of icons and even adjust it if we have some special needs (like we need a filled star for some cases we already have).
    The final sprite map in my browser tests is about ~550kiB when only using gzip and with zstd I reach 450kiB.

    To make use of it and have something to test I also created a new component to render it:

    nextcloud-libraries/nextcloud-vue#8264

    This component allows to render those icons by name so that we could just use API responses with icon names.
    You can play with it in the styleguide:

    https://deploy-preview-8264--nextcloud-vue-components.netlify.app/#/Components/NcIcon

  21. max-nextcloud commented on Feb 28, 2026

    @max-nextcloud

    I wonder if it would be useful to have two sets of icons and also two sprites:

    • nextcloud with all the icons that are known to be used.
    • material with the material symbols.

    nextcloud would be significantly smaller and if only icons from that set are used initial load would be slightly faster.

    If one needs a new icon for an app:

    1. Check if a fitting icon is in nextcloud. Use that and avoid adding another slightly different variant.
    2. If there's none start with using the icon from material
    3. Create a PR to add the icon to nextcloud.
    4. Once the icon is available in all server versions supported by the app use it from nextcloud instead.
  22. susnux commented on Mar 2, 2026

    @susnux
    Author

    While that could work it would either need to keep track of the state of such icons, either a pretty large lookup table or another web request to load both sets. But if we load both sets then there is no benefit because then both are cached.

    I think this would complicate maintenance (watch for new icons, add them, adjust lookup logic, what about removal - will it break existing apps?), while the benefits are not that huge because all that we can ideally reduce is the initial load on first visit because after first visit this is cached by the browser.

    Also note that this is not javascript but purely an image file, meaning it will be fetched asynchronously by the browser and will neither block rendering nor any other process unlike with loading of scripts.


    Quick comparison:
    Currently we ship in vanilla server

    • ~310kb icons CSS
    • ~100kb icons in various javascript files

    So we would only gain additional ~100kb in page load size, but this time only images which are loaded independent of scripts. And would cover all icons, meaning also 3rdparty apps would be smaller because they do not need their own icons anymore.

  23. max-nextcloud commented on Mar 2, 2026

    @max-nextcloud

    I do see the additional overhead. I'm not sure I understand what you mean by 'lookup table'. I'd imagine something like listing the icons included in nextcloud in https://github.com/susnux/nextcloud-icons/blob/main/build/custom-mapping.json. This would then enable building two svg files.

    My thinking is that this list of icons known to be used might be useful for customizing or migrating the icons later on. Otherwise it's probably not worth the extra effort.

    I would not worry too much about removal - having a few extra icons stick around seems okay.

    In terms of caching I'm somewhat worried about scenarios where caching does not work. Public file shares come to mind. A lot of peoples first (and only) contact with a Nextcloud instance is downloading some shared file. If that's perceived as slow the impression may stick. Another example I recently learned about is that caching text app assets in the ios notes app is broken. As each note opens a webview every time the entire js is downloaded again - which makes the loading of the editor very slow. Of course this is a separate problem that needs to be addressed. It just made me aware again of the fact that we cannot always rely on caching.

    Just my 2 cents. I don't mean to delay this discussing details. Having two lists seems like another optimization that could as well be applied later when deemed necessary.

  24. susnux commented on Mar 3, 2026

    @susnux
    Author

    I'm not sure I understand what you mean by 'lookup table'

    Well if you pass any icon name then we would need to know:

    • does this Nextcloud version ship this icon in "nextcloud-icons" or do I have to use the "all icons" bundle.

    So if we add a new icon then this lookup table from "nextcloud version" -> "which bundle for this this icon" would grow.
    Currently we "support" 5+ versions of Nextcloud, if we allow adding icons in patch version we would have a lookup table of up to 70 entries to find the bundle to use for an icon.
    But of course we can also always first try nextcloud bundle.

    I would not worry too much about removal - having a few extra icons stick around seems okay.

    Might be true, but a few easily dozens, as we regulary change icons and use new ones etc.
    Which is also the problem I see with that 2 files solution:
    We really often add new icons in apps or at least change them, meaning you likely end up with loading the full bundle anyways.

    In terms of caching I'm somewhat worried about scenarios where caching does not work. Public file shares come to mind.

    Yes two options for empty cache (except bugs like with the webview you mentioned):

    • first visit login
    • first visit public page

    But unlike javascript cache this is only about image cache, meaning the user would not notice any delay in page load.
    Yes the icons might be shown a bit later, but the page is at least already fully interactable.

    I don't mean to delay this discussing details

    No dont worry!
    I highly appreciate any input here and think its really important to discuss such potential performance topics!

    Having two lists seems like another optimization that could as well be applied later when deemed necessary.

    Yes I also think we can add this we notice there is a performance issue.

  25. sorbaugh commented on Jun 30, 2026

    @sorbaugh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions