RFC: consistent icon system across apps and server #16
Description
Activity
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.
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?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 recentoutlinechanges), 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.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.
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.
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)
Reacted by Jan C. BorchardtThere 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.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:
- Library (can be a large set, everybody uses what they need)
- 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".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 uncompressedSo, is the proposal about bundling all the symbols or a very limited subset? 380 is about 10% of Material Symbols.
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?
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 issuesIMO 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 🥲Reacted by Christoph Wurstusing @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.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/imgthen 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:
- A method to generate an SVG string for use with current inline-SVG APIs¹
- Add it to
NcIconSvgWrapper(or addNcIcon)
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 callgetSvgIcon('celebration'). Meaning no longer any need to import anything :)Reacted by John Molakvoæ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.
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
If we serve as svgz, it's down to
786K545K
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.gzScript
#!/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"
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.
Reacted by Jan C. Borchardt and Ferdinand ThiessenAlso 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.
Reacted by Ferdinand Thiessen- added 2 commits that reference this issue
on Feb 28, 2026 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
Reacted by Andy Scherzinger, max-nextcloud and John MolakvoæI wonder if it would be useful to have two sets of icons and also two sprites:
nextcloudwith all the icons that are known to be used.materialwith the material symbols.
nextcloudwould 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:
- Check if a fitting icon is in
nextcloud. Use that and avoid adding another slightly different variant. - If there's none start with using the icon from
material - Create a PR to add the icon to
nextcloud. - Once the icon is available in all server versions supported by the app use it from
nextcloudinstead.
Reacted by Andy ScherzingerWhile 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.
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
nextcloudin 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.
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.
Reacted by max-nextcloud and John Molakvoæcc @kra-mo
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:
vue-material-design-icons-> Vue components of SVGs@mdi/svg-> use of inline SVG files which are then rendered by e.g.NcIconSvgWrapper@mdi/js-> inline SVG but we try to circumvent the problem of bigger bundle size due to inline SVGs by only using thepathof the SVG.To sum up:
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/iconsSVG packageUnify 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.
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:This file can easily be generated by already existing tools.
Then in your HTML you can refer the icon you like with:
The benefit here is that any migration would be quite easy:
To sum up this would - from my point of view - make working with icons much easier:
Further reference