Icons: Redraw 35 prominent icons to strokes, and maintain absolute stroke-width#78808
Icons: Redraw 35 prominent icons to strokes, and maintain absolute stroke-width#78808jasmussen wants to merge 18 commits into
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
A note that I forgot to mention: as I carefully hand-redraw these, some curves and vectors become more precisely positioned, clearly mistakes from previous iteration. In 95% of the cases in this batch, the end result when seen at 24x24 is invisible. However I want to point out a couple of icons were very slightly visually refreshed as part of this, in case that invites feedback. IMO, it's not enough to warrant a changelog entry beyond what this PR can benefit from regardless, but for your awareness:
|
|
Flaky tests detected in 932a439. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27336025003
|
|
Wonderful! Since this involves redrawing icons, would it make sense to address some of the low-hanging fruit in #65786 as well? For instance applying endcap-roundness consistently, pixel grid alignment, and footprint consistency? |
|
Yes it does indeed! And notably the pixel grid alignment I've been fixing here. There's been a lot discovered. I've also unified a couple of circles that were off-size. I'm hesitant to do too much, but would be happy to follow up after we land a good new stroke based baseline. One really potent improvement as I do this: the new icons are subtantially easier to edit and work with, because what we used to call the "icon source", the stroke based version we would go ahead and convert to fills, that is now the icon that we ship, one and the same, so the Figma to GitHub flow is now very much improved. The short is, once this is done, it should be both easier to contribute, to edit, to consolidate, and all that. |
|
Yes, appreciate it's a balance; we want to avoid scope creep. I'm just aware that it might be easier to tweak these things in one go rather than sequentially, with no meaningful trade-offs that I can think of. One other thing that springs to mind from a previous exploration around converting the icons to be stroke-based... there is quite a bit of css in Gutenberg that targets icons with |
d336873 to
9c97f83
Compare
|
|
||
| svg, | ||
| path { | ||
| svg { |
There was a problem hiding this comment.
This change was necessary because without it, everything gets a fill, including what is meant to just be a stroke based circle (with a hole in the middle).
I prefer to keep PRs minimal, but this one was necessary in order for this PR to be possible to land. I have some additional changes I want to push—a normalisation script and some more resilience, but this was a minimal change I was able to do. I wasn't able to find any ill consequences from this change, but CC'ing some component experts for a gut check: @mirka @ciampo
There was a problem hiding this comment.
I trust @ciampo's judgment here but just wanted to confirm that this has been tested and verified to not cause any unexpected consequences.
| <HStack justify="flex-start"> | ||
| { icon && ( | ||
| <Icon | ||
| style={ { fill: 'currentcolor' } } |
There was a problem hiding this comment.
This rule overrode the SVG that was inserted, and forced a fill rule on it, making it impossible to use stroke-based icons.
| return `${ camelKey }: '${ value }'`; | ||
| } ) | ||
| .join( ', ' ); | ||
| return ` style={ { ${ declarations } } }`; |
There was a problem hiding this comment.
This change was required so the inline style="fill: none" on stroke icons gets translated to React's object form during build. Without it, React ignores the style and the icons render filled.
Claude Code helped me with this, btw.
|
Size Change: +1.43 kB (+0.02%) Total Size: 8.59 MB 📦 View Changed
|
| @@ -1,3 +1,3 @@ | |||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> | |||
| <path d="M18.5 5.5V8H20V5.5h2.5V4H20V1.5h-1.5V4H16v1.5h2.5zM12 4H6a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2v-6h-1.5v6a.5.5 0 01-.5.5H6a.5.5 0 01-.5-.5V6a.5.5 0 01.5-.5h6V4z" /> | |||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="fill: none" stroke="currentColor" stroke-width="1.5"> | |||
There was a problem hiding this comment.
We're now adding a lot of explicit currentColor in the icons themselves. This is going to result in inconsistent behavior across icons.
Here's the icon library in a color: red context:
| Before | After |
|---|---|
![]() |
![]() |
Unlike Icon in either wp-components or wp-icons, the new Icon from wp-ui applies a fill="currentColor" by default. Here's the icon library modified to use the wp-ui version:
If we're going with these stroke-based icons, I think we're probably forced to add explicit currentColor in the icons themselves, for all the icons, not just the ones that have been redrawn here.
There was a problem hiding this comment.
If we're going with these stroke-based icons, I think we're probably forced to add explicit currentColor in the icons themselves, for all the icons, not just the ones that have been redrawn here.
I volunteer to do that 🙋♂️
Before I do, I want to just check with you if you're okay with this? Is it functionally a good choice? IMO yes as this is the implied behavior of the icon set and it's worth making explicit. I also expect to follow up with a PR that adds a script to do this on every build of the set.
There was a problem hiding this comment.
I think it's a good choice. It just has the potential to introduce unexpected visual glitches for consumers receiving this update, but it's probably for the best in the long term and it alings the current behaviour to the new behaviour that @wordpress/ui's Icon component already applies
There was a problem hiding this comment.
It will also expose a cleaner way to change the icon color: ie. via CSS color, rather than guessing if fill or stroke should be used
There was a problem hiding this comment.
I agree. I think it's ultimately a tech enhancement that opens up new opportunities.
In that vein, what are some action items for this PR? Do you think we can land this one as-is, or do I need to include all the icons in one go?
3568808 to
5d10653
Compare
Both the components and icons Icon components replaced an icon's intrinsic style entirely when a consumer passed a style prop, stripping things like fill: none from stroke-based icons. Merge the two instead, so consumer styles layer on top and only override on explicit conflicts. Adds regression tests and corrects the changelog PR links to #78808. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
932a439 to
19760b5
Compare
|
@t-hamano Thank you for all the help. I've rebased this to pick up changes from conflicting changelogs. I tested your steps, everything seems to work. Based on the stroke-width being flexible I suspect we might want to update the changelog for the Icon block at least, to indicate what changed. I'm also happy to follow up with a new feature addition to the Icon block, to add a stroke-width slider to the block. What do you think is the best approach there? |
|
Noting that if we did want to temporarily turn off stroke-based scaling, I'd prefer to do it in a separate PR that theoretically could be as little as this: |
|
Noting that I created #79116 that we can land before this PR, to maintain the visuals of the icon block even as I complete the stroke-based conversion. |
| // Split on the first colon only: values can contain colons too | ||
| // (e.g. a `data:` URI), so only the first separates key/value. | ||
| const colonIndex = decl.indexOf( ':' ); |
There was a problem hiding this comment.
Since splitStyleDeclarations is already going to the trouble of tokenizing the CSS, it would seem preferable to have it return a Map of properties, or an array of [ key, value ] tuples. Then this wouldn't have to try to parse out the value here.
There was a problem hiding this comment.
Thanks for all the suggestions. I've tried to apply them, and address the feedback here. Let me know if I got this right.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Lena Morita <lena@jaguchi.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both the components and icons Icon components replaced an icon's intrinsic style entirely when a consumer passed a style prop, stripping things like fill: none from stroke-based icons. Merge the two instead, so consumer styles layer on top and only override on explicit conflicts. Adds regression tests and corrects the changelog PR links to #78808. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CSS-string to JSX style-object conversion now skips malformed declarations and escapes values via JSON.stringify, and splits the declaration list with a tokenizer that ignores semicolons inside url() and quoted strings (so base64 data: URIs survive). Adds unit tests covering the splitter and the end-to-end conversion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the same explicit style merge to the isValidElement fallback so a non-svg icon element's intrinsic style (e.g. fill: none) survives a consumer-supplied style prop, matching the svg branch. Adds a regression test, and tidies changelog spacing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The icon registry sanitizes SVG content through wp_kses with an allowlist that omitted stroke-related attributes and inline styles. Stroke-based icons rely on stroke, stroke-width, stroke-linecap, stroke-linejoin, stroke-miterlimit, vector-effect, and style="fill: none", so server-side rendering stripped them and the icons rendered as solid black fills. Add these attributes to the svg and path allowlists in both the compat base class and the Gutenberg subclass override, so stroke icons render correctly regardless of which base class core provides. Co-Authored-By: Claude <noreply@anthropic.com>
Even with `style` in the kses allowlist, wp_kses filters the attribute value through safecss_filter_attr(), which drops CSS properties not on its allowlist. SVG presentation properties like `fill` are not allowed, so `style="fill: none"` was emptied and removed during sanitization, leaving stroke-based icons with a solid default fill. Temporarily register a safe_style_css filter while sanitizing to allow the SVG presentation properties (fill, stroke, stroke-width, stroke-linecap, stroke-linejoin, stroke-miterlimit) in both the compat base class and the Gutenberg subclass override. Additionally, the icon block overwrote the SVG's intrinsic style attribute when applying block styles. Merge the existing style with the generated CSS so `fill: none` survives, with block styles last to win on conflicts. Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Weston Ruter <westonruter@git.wordpress.org>
Co-authored-by: Weston Ruter <westonruter@git.wordpress.org>
Co-authored-by: Weston Ruter <westonruter@git.wordpress.org>
A previous suggestion application landed the JSDoc body without its opening /** delimiter, which left the file with orphaned * lines that break Node's parser. Restore the opener so the build runs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rename splitStyleDeclarations to parseStyleDeclarations and have it split each declaration on the first un-quoted, un-parenthesised colon as part of the same tokenisation pass. Returns Array<[key, value]> instead of string[], so the caller no longer needs a second indexOf parse to extract the value. Per Weston's review feedback. Adds tests for the colon-handling edge cases (colons inside parens, quotes, and `data:` URIs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ed03e5d to
2e2bdf5
Compare
| // `wp_kses()` filters the `style` attribute value through | ||
| // `safecss_filter_attr()`, which only keeps allowlisted CSS properties. | ||
| // SVG presentation properties such as `fill` (e.g. `style="fill: none"` | ||
| // on stroke-based icons) are not in the default allowlist, so temporarily | ||
| // allow them while sanitizing. | ||
| $allow_svg_css = static function ( $properties ) { | ||
| return array_merge( | ||
| $properties, | ||
| array( | ||
| 'fill', | ||
| 'stroke', | ||
| 'stroke-width', | ||
| 'stroke-linecap', | ||
| 'stroke-linejoin', | ||
| 'stroke-miterlimit', | ||
| ) | ||
| ); | ||
| }; | ||
|
|
||
| add_filter( 'safe_style_css', $allow_svg_css ); | ||
| $sanitized_content = wp_kses( $icon_content, $allowed_tags ); | ||
| remove_filter( 'safe_style_css', $allow_svg_css ); |
There was a problem hiding this comment.
Agreed, this should be addressed.
Maybe we should declare it in a single place so it is easier to replace later?
Also, let's make sure we add tests. This is an important change that warrants tests.
| 'stroke-miterlimit' => true, | ||
| 'vector-effect' => true, | ||
| ), | ||
| 'polygon' => array( |
There was a problem hiding this comment.
Any reason to miss adding the above attributes to polygon as well?
| * @param string $icon_content The icon SVG content to sanitize. | ||
| * @return string The sanitized icon SVG content. | ||
| */ | ||
| protected function sanitize_icon_content( string $icon_content ): string { |
There was a problem hiding this comment.
Is there a better way to avoid this one being duplicated in multiple places?
| // stroke-based icons) so it is preserved. The block styles come last so | ||
| // they win on any conflicting property. | ||
| $existing_style = $processor->get_attribute( 'style' ); | ||
| $merged_style = is_string( $existing_style ) |
There was a problem hiding this comment.
Should we check the length of the style attribute too? If it's an empty attribute, won't we get a leading ;?
|
|
||
| svg, | ||
| path { | ||
| svg { |
There was a problem hiding this comment.
I trust @ciampo's judgment here but just wanted to confirm that this has been tested and verified to not cause any unexpected consequences.
| ...( icon.props as { style?: CSSProperties } ).style, | ||
| ...consumerStyle, | ||
| }, | ||
| ...restProps, |
There was a problem hiding this comment.
For consistency, we might want to move ...restProps above style here, or the other way around in the other place.
| $merged_style = is_string( $existing_style ) | ||
| ? rtrim( trim( $existing_style ), ';' ) . '; ' . $styles['css'] | ||
| : $styles['css']; | ||
| $processor->set_attribute( 'style', $merged_style ); |
There was a problem hiding this comment.
May be something else that needs a test.
| * @param string $icon_content The icon SVG content to sanitize. | ||
| * @return string The sanitized icon SVG content. | ||
| */ | ||
| protected function sanitize_icon_content( string $icon_content ): string { |





What?
Followup/replacement to #78774, which now includes the change including resilience for compatibility.
In this PR, I redrew/changed 34 icons to be stroke-based rather than purely fill-based. An existing icon was already stroke-based,
square. This brings an enormous amount of flexibility as far as future animation and customisation properties, it simplifies the Figma to Icon flow, but notably it means that icons now maintain their stroke-width across sizes. Paragraph shown as example, 16, 24, 32px sizes:Why?
For the majority of these icons, no visual change should be perceptible in where they are used today, because we use them mostly in their standard 24x24px sizes. And we do that specifically because of the shortcoming in how they scale, shifting stroke-width making them look out of place next to each other.
By enforcing a monoline stroke-width, icons can now be shown at virtually arbitrary sizes (though they can get fuzzy if shown really small), and yet still feel as if they belong together since their stroke-widths are maintained. A future perspective is even to allow consumers of the componentry to either animate the stroke, both the width and the dash-style, offering some interesting options, such as how this WordPress logo is animated.
How?
This PR includes both the tech to maintain a uniform stroke-width across icon dimensions, as well as 35 icons that take advantage of it. I am working in a WIP Figma file to go through all existing icons, as such:
In black, those are the icons in this PR, done/redrawn. In red, still todo. In blue, icons we will not be redrawing as they are not/can't be stroke-based. In gray, icons that need redesigns anyway. Once this effort is done, these icons will move to the main Figma design library. I believe there are some additional icons shipping in trunk that are not depicted in this list, but I will be sure to identify those once I'm through this effort.
Testing Instructions
Check out the PR; run npm run storybook:dev, go to Icons > Library, and then observe that at both 16, 24, and 32, that icons look as intended. These icons:
Use of AI Tools
Claude Code was used to extract a todo-list of icons for me, but the rest is hand-rolled, human spirit work.