-
-
Notifications
You must be signed in to change notification settings - Fork 32.6k
Create MUI Symbols Library #46183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
dav-is
wants to merge
44
commits into
mui:master
Choose a base branch
from
dav-is:create-mui-symbols-material
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Create MUI Symbols Library #46183
+206,866
−2
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Note
This PR is a draft and still a work in progress. See Todos at the end of the description.
Related issues: #32846 & #42704
Try the new icon picker
Implementation
There can be a benefit to using the icon font, so we can't recommend using SVGs in all cases. To put the decision in the hands of the user, I would recommend creating a unified API surface between the icon font and SVGs. The user would learn a single way of adding icons to their app, and switching between SVGs and the icon font would be as simple as changing the import. If using the icon font, they would gain animations seamlessly. If using SVGs, icons would render instantly on the first load. It's worth noting that the icon font can still "code-split" the icons at build time with the right tooling. I think it would also be fair for applications that have dynamic icon usage (e.g. user-defined category tagging), icon fonts would be a better solution.
Differentiating between "theme" properties and "icon" properties would be the solution here. In my opinion,
weight
androunded|outlined|sharp
would be theme properties defined at an app level in most cases.The user would import their icons like this on the most basic level to use our given theme defaults:
A customized theme would look like this:
This could be enforced by a linter rule. An import alias could also be used to have
@symbols/Search
point to@mui/symbols-sharp-300/Search
.Further customizations (
size
,fill
,grade
) of the icon would occur based on context.emphasis="strong"
oremphasis=true
would translate tograde
being set to200
.emphasis="muted"
would translate tograde
being set to-25
.emphasis="normal"
(default) would translate tograde
being set to0
.filled=true
would translate tofill
being set to1
.filled=false
would translate tofill
being set to0
.filled=0.5
would translate tofill
being set to0.5
for the icon font and 1 for SVGs. (Alternatively, it might make sense to show unfilled for all values less than 1 when using SVGs)fontSize=small
would translate to a size of20px
fontSize=medium
would translate to a size of24px
fontSize=large
would translate to a size of40px
fontSize=x-large
would translate to a size of48px
fontSize=16px
would translate to a size of16px
andoptical-size
of20px
(calculated by range)fontSize=2rem
would translate to a size of32px
andoptical-size
of40x
(calculated by range)fontSize=3rem
would translate to a size of48px
andoptical-size
of48x
(exact match will be sharpest)Switching to the icon font could be as simple as changing the import statement (or updating the import alias):
This requires a new icon component:
VariableIcon
. It abstracts away the complexities ofSVGIcon
to allow the user to choose between the icon font or SVGs. When using Material Symbols, I don't think it makes sense for users to use advanced SVG configurations. We should handle that for them.To generate these
VariableIcon
components, we have a helper functioncreateVariableIconFromSvg
which wraps theVariableIcon
component and passed aSVGIcon
component to it (so we still benefit from the optimizations there). We also have thecreateVariableIconFromFontString
function that creates the component using a string for use with the icon font instead of the SVG. TheVariableIcon
always wraps its child with a<span>
element. This is for styling consitancy between the font using<span>icon_name</span
and the SVG using<span><svg>[...]</svg></span>
and protects the svg element from being directly modified.When using the icon font, there are also helper components for adding the stylesheet.
You can also filter by individual icon:
Each theme has a component (e.g.
<MUISymbolsSharp300GoogleFonts />
).For users who value performance over everything, I think we should also offer
@mui/symbols-lite
which would only bundle filled and unfilled states and only the optical size of 24px. Lite icons would not allow the use of theemphasis
prop, in order to encourage the use of the more performant filled state.For users who want a combination of instant icon loading, but still want icon animations, we could offer
@mui/symbols-progressive
. This would load the SVG and server render it, but after the font was detected as loaded, it could swap to the icon. This would mirror the behavior offont-display: swap
and avoid the shortcomings of the icon font showing the icon text when the font takes too long to load. After the font is loaded, you could animated fill, particularly useful for the main navigation icons. This can still be performant because the icon font can be used with a whitelist of the handful of icons used. Unfortunately, thelite
icons couldn't be used with this because with each page navigation, the icons would shift in weight.Pros
fill
andunfilled
to convey stategrade
propertyfill={progress}
) instead of using a separate icon for each state@mui/symbols
- instant loading, high fidelity, auto codesplitting, higher js footprint@mui/symbols-font
- deferred loading, animations, icon selector components / user defined icons, manual codesplitting (icon whitelist), smaller html footprint@mui/symbols-progressive
- instant loading, animations, navigation icons, heavier footprint (html, js, and font)@mui/symbols-lite
- instant loading, low fidelity / inconsistent, smallest footprintCons
@mui/icons-material
. Given Material Surfaces is not Material Icons v2, I think this is acceptable.SVGIcon
component.@mui/symbols-lite
package.@mui/symbols-sharp-300/SearchSmall
), but this would reduce our ability to automatically switch the optical size based on the size of the icon. This might be acceptable given it doesn't easily animate. It would reduce the usefulness of autocomplete. It might be confusing to users why they have a "optical size" and a "font size". [Compromise A]@mui/symbols-sharp-300/SearchBold
). This would limit our ability to automatically use-25
grade when in dark mode. [Compromise B]grade
into two components:@mui/symbols-sharp-300/Search
and@mui/symbols-sharp-300/SearchBold
. This would allow us to use the-25
grade in dark mode. [Compromise C]Size Benchmarking
I selected an icon (
TwoKPlus
) to test the impacts on size:@mui/icons-material
around470 bytes
(440 bytes
compressed, 94%)@mui/symbols/Home
(optical-size
+grade
+fill
) -12,400 bytes
(4,000 bytes
compressed, 32%, 9X larger than MUI Icons)@mui/symbols/Home(Small|Medium|Large|Larger)
(grade
+fill
) -3,027 bytes
(1,092 bytes
compressed, 36%, 2.5X larger than MUI Icons)@mui/symbols/Home(Muted|Regular|Strong)
(optical-size
+fill
) -3,852 bytes
(1,528 bytes
compressed, 40%, 3.5X larger than MUI Icons)grade
@mui/symbols/Home
(optical-size
+grade:normal|muted
+fill
) -8,154 bytes
(3,144 bytes
compressed, 39%, 7X larger than MUI Icons)@mui/symbols/HomeBold
(optical-size
+grade:strong
+fill
), -4,392 bytes
(1,732 bytes
compressed, 39%, 4X larger than MUI Icons)It's worth noting that this would be the added weight on the Javascript bundle, not the initial HTML payload. Only a single SVG would be rendered in the HTML. These bundled SVGs benefit a lot from gzip compression which makes sense because each variant is very similar. It's safe to assume any modern application will be using gzip compression for the JS bundles.
It's estimated that a
@mui/symbols-lite
icon (optical-size:24
+grade:0
+fill
, only two variants) could be reduced to ~1,000 bytes
(660 byte
compressed, 66%, 1.5X larger than MUI Icons)Icon Selection Docs Page
Because the icons page needs to load all the icon variants, doing so with the package would require bundling 75k modules. When I attempted this, the page would not build, even when using the font icon version. This is not an issue with the packaging, because the typical user's icon selection component would likely only use one theme (e.g. sharp-300) and would only be 3,600 modules.
The docs load the theme and weight that is selected using the Google fonts stylesheet. When selecting a weight it will load the range. For example, the default is outlined-400. If you select outlined-500, it will load the weight range 400-500. If you select 700, it will load 400-700. This is due to a limitation in Google font loading. If you have outlined-400 selected and switch to rounded-400, it will just load rounded with a weight of 400. During this loading, the icons will show a animated skeleton.
The bundle size of this page is greatly reduced because the SVGs are not included. I would argue there isn't much benefit of loading the icons instantly. Repeat visitors of the page should have their icons load instantly because they have the font cached. If SVGs were used, it wouldn't be as easy to cache all the icons as they are baked into the html each time it is loaded.
When opening the icon dialog to see a single symbol, you are able to vary the "context" parameters that are set on the icon component directly:
emphasis
andfill
. The fill component is nicely animated. You can also decide between the font and SVG version of the icon. A newx-large
size shows off the48px
version of the icon. You'll notice each size looks roughly similar, where the@mui/icons-material
icons seem bolder as they grow.You notice when the icon is sized up to 200px in the icon dialog, it looks significantly better than
@mui/icons-material
. This is because the optical size. This is great to showcase for when icons are used in dialogs, like when you want to warn the user with a large caution icon.There are likely more performance optimizations that could be made for this page. The virtual scrolling optimization was removed in favor of
content-visability: auto
, but this probably needs more testing. The loading skeleton, might add too many DOM elements to be worth it and could be optimized too.Considerations
Optical Size
Optical size needs to be set based on the actual icon size so that the icon is consistent with other icons of different sizes. For example, if you used a large icon on a page with many small icons, the large icon would look much bolder than the small icons. Google has a helpful animation that shows this effect here.
Another benefit of the optical size is 20px icons are slightly simplified compared to 24px icons to be sharper on the screen and avoid aliasing. The docs in the material symbols repo says "What is currently not available in Material Symbols? only the 20 and 24 px versions are designed with perfect pixel-grid alignment" This alludes that 40px and 48px icons will eventually be pixel perfect. Justifying us using these sizes are default values for
large
andx-large
icons to increase sharpness. It's worth noting that today with theSVGIcon
component,large
translates to 35px andx-large
doesn't exist. I think we should make this change before it would be breaking for@mui/symbols
users.To quote someone from the Google team, "Also, no stroking change can correctly take you from the 24 px master to the 20 px master or vice versa, when each of them is manually designed to fall precisely on the pixel grid. If you are taking a skeleton and stroking it, and generating as SVGs, either all your 24 px icons will look fuzzy, or all your 20 px icons will look fuzzy. We just spent a big chunk of the last eight months or so revising the 20 px masters to be grid-snapped based on manually tweaked pixel patterns and outlines."
Source
For the sizes that are not exactly the same as the optical size, Google's icon selector automatically switches the optical size based on these ranges:
If the size is in rem, how do we know what the optical size should be? We could set the optical size based on a base font size of 16px, but if a user increases their font size, they would effectively be increasing the weight of the icons as well. This seems like an ok tradeoff. We could possibly use
getComputedStyle
to get the font size of the element and calculate the optical size based on that, but that would complicate the possibility of server rendering SVGs. If a user has their font size increased, they would see a flash of a bold icon before the react tree hydrates and rerenders based ongetComputedStyle
.If animating the size, when do we switch the optical size? Before or after the animation, or would we recommend using
requestAnimationFrame
to switch at the threshold Google uses? If animating the size, it might make sense to keep a static optical size to avoid the icon slightly changing during the animation. Given the SVG icon size can't be easily animated because of this, would this make a case for having a separate icon file for each optical size? Then again, including the size in the import makes the user grapple with "what is an optical size?" If the size is in the import, I would recommend only allowing thefontSize
to be set within the range shown above. This would enable small growth or shrinking animations for emphasis, but not a full size change.In the end, I settled on a CSS only approach which switches the SVG based on a
@container
query. Which allows us to inherit the font size using CSS as the user would expect. It also supports rem units nicely, which is useful when the font size is increaded (or decreased), the correct optical size would be used. Also, since it uses CSS, there isn't a flash of bolded icon before the JS hydrates the page.A benefit of the icon font is that the optical size is set automatically.
Weight
Weights 100-700 are available. We might want to expose them as their names to be more familiar and descriptive:
In the Material 3 spec, weight 100 is not allowed to be used with icons that are smaller than 24px. Source
The spec also forbids mixing weights within a set of icons, so it wouldn't make sense to use weight as a indicator of which icon is selected. Source
It also seems using
stroke-weight
instead of individual SVGs is not feasible. SourceGrade
We have -25, 0, 200 for grades. It gives even more variance on weight. It seems the intended use of this would be to take a theme weight of 400, and allow "muted", "normal", and "strong" variants of the icon. This makes me think
weight
is a theme property, andgrade
is an icon property based on the context of the icon.In the Material Symbol docs, it mentions, "To highlight a symbol, increase the positive grade." Source
It's worth noting that the Material 3 spec recommends using
-25
grade when in dark mode to reduce glare. This would justify bundling grade in a single component as it is dependant on context. SourceFill
fill
is a variable that was not mentioned in #42704. If following the recommendation of that issue, it seems we would export/variants/sharp/100/neg25/filled
(with combined optical size of 20, 24, 40, 48 in one file).In the Material 3 spec, fill is mentioned as an indicator to show it has been selected (Source). In the icon font, the switching between
fill
andunfilled
has a deliberate animation. The employee from Google mentioned that these animations cannot be achieved with SVGs. Would that make a case for having filled and unfilled icons in the same file? Having no animation support for icons means there is a serious benefit to using the icon font.Mentioned in the Material 3 spec, it says, "A fill attribute can be used to convey a state of transition, such as unfilled and filled states. Values range from 0 to 1, with 1 being completely filled." That seems to allude that you could set fill to
0.5
and it would have a half-filled icon. This further justifies that fill would be an icon property, not a theme property.Editor Autocomplete
If I type
<Home />
into my editor, it would be nice to autocomplete the import according to the icon theme I am using. We could achieve this by installing only one theme at a time. Instead of installing@mui/symbols
, we could ask the users to install@mui/symbols-rounded-300
or@mui/symbols-rounded
. If two weights were desired, the user would only see two options forHome
instead of all the theme variants.I think it also make sense to call the module
@mui/symbols
instead of@mui/symbols-material
so that it is shorter. It seems we intend on adding our own icons and removing some that don't work well, so it would make sense to deviate a little in the name as well.Module Size
Someone from the Google team mentioned that all the possible variants add up to more than a million SVG files. Source
By my count, there are
468
SVG files per icon with156
files for each style (Rounded|Outlined|Sharp
). With3618
icons, that's1,693,224
SVG files.This might not scale very well and could drastically increase the install time of the module. We should consider splitting into multiple packages like mentioned above, or reducing the number of files by combining the parameters that depend on the context of the icon.
As proposed, this would result in
75,978
Javascript files built. If the package is split byrounded|outlined|sharp
(e.g.@mui/symbols-sharp
), that would be25,326
files. Since the proposal also splits byweight
(e.g.@mui/symbols-sharp-300
), each module only has as many files as icons (3618
). This is a lot more manageable than1,693,224
files. Additional weights can be installed as needed making it modular.It's estimated that the size of
@mui/symbols-sharp-300
would be around40MB
uncompressed with ESM only. The compressed size should be closer to13MB
. This is significantly larger than@mui/icons-material
which seems to be a little larger than4MB
. It also has about 1500 additional icons though.Checking Files into Git
Are we able to check 1.6 million SVG files into git? It seems for
@mui/icons-material
there are also CommonJS and ESM versions for each SVG file. If we use the same pattern for Material Symbols, that's5,079,672
files checked into git. Is this for monorepo reasons?It seems clear that we need to combine some of these SVG files into a bundled file, see recommendations below. I also think git should only contain one version of either the SVG or JS file, instead of duplicating three times. The build output itself should be stored on NPM only.
To demonstrate the scale of the build output, I created a sub-PR with only the
@mui/symbols-font
packages built: #46194 I think the SVG version was just to big to include.I understand that each version of the icons may need to be available through git, so maybe a solution here could be to use the Google repo as a submodule.
I think the best solution would be to create a new repo just for generating these JS files that contain SVGs. All the SVGs consume
1.5 GB
. That's not realistic to store within the monorepo. For LTS reasons we will need an archive of previous icon releases. Google's API doesn't have a versioned endpoint, so we can only fetch the latest SVGs. We can release new versions of the symbols within that repo instead of weighing down the main repo. In the separate repo we would publish@mui/symbols-svg-sharp-300
version0.1.0
. Within the monorepo, we would publish@mui/symbols-sharp-300
version7.1.0
in sync with the current MUI version. We should also archive the icon fonts in this repo and package them to avoid Google removing icons in the future and breaking our packages.TODO
mui-symbols
package@mui/symbols/Search
)VariableIcon
componentcreateVariableIconFromSvg
function/material-ui/api/variable-icon/
api page/material-ui/material-symbols/
docs pagecreateVariableIconFromFontString
function@mui/symbols-font
version of the icons@mui/symbols-font
(not for SVG-based due to bundle concerns)/material-ui/material-symbols/
workingfont-size: inherit
and if it should be allowed (yes it should).MuiVariableIcon-fontSizeSmall
) are dangerous (not with@container
)@container
queries to avoid JS and supportrem
units/material-ui/icons/#variableicon
docs page/material-ui/icons/#font-vs-svgs-which-approach-to-use
to detailVariableIcon
VariableIcon
component100
,200
,300
, etc withthin
,ultralight
,light
, etc@mui/symbols-cjs
(they can't both be included because of size concerns)@mui/symbols-progressive
@mui/symbols-lite