Skip to content

Icons: Redraw 35 prominent icons to strokes, and maintain absolute stroke-width#78808

Open
jasmussen wants to merge 18 commits into
trunkfrom
update/redraw-34-icons-to-stroke-based
Open

Icons: Redraw 35 prominent icons to strokes, and maintain absolute stroke-width#78808
jasmussen wants to merge 18 commits into
trunkfrom
update/redraw-34-icons-to-stroke-based

Conversation

@jasmussen

@jasmussen jasmussen commented May 29, 2026

Copy link
Copy Markdown
Contributor

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:

Skærmbillede 2026-05-29 kl  12 25 11 Skærmbillede 2026-05-29 kl  12 25 17 Skærmbillede 2026-05-29 kl  12 25 42

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:

Icons

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:

  • add-card
  • add-template
  • cancel-circle-filled
  • caution
  • caution-filled
  • code
  • comment-author-avatar
  • cover
  • currency-dollar
  • currency-euro
  • currency-pound
  • drafts
  • help
  • help-filled
  • image
  • info
  • lifesaver
  • link
  • link-off
  • navigation
  • not-allowed
  • paragraph
  • pending
  • plus-circle
  • plus-circle-filled
  • published
  • scheduled
  • site-logo
  • star-empty
  • star-filled
  • star-half
  • styles
  • square
  • time
  • tip

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.

@jasmussen jasmussen requested review from a team and t-hamano May 29, 2026 10:35
@jasmussen jasmussen self-assigned this May 29, 2026
@jasmussen jasmussen added [Type] Enhancement A suggestion for improvement. [Package] Icons /packages/icons [Feature] Icons Related to Icon registration API and Icon REST API labels May 29, 2026
@jasmussen jasmussen marked this pull request as ready for review May 29, 2026 10:35
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: jasmussen <joen@git.wordpress.org>
Co-authored-by: t-hamano <wildworks@git.wordpress.org>
Co-authored-by: mirka <0mirka00@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
Co-authored-by: westonruter <westonruter@git.wordpress.org>
Co-authored-by: jameskoster <jameskoster@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>
Co-authored-by: fcoveram <fcoveram@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@jasmussen

Copy link
Copy Markdown
Contributor Author

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:

  • time-to-read has changed from using sharp hands to using the same style as that of the "pending" clock
  • the stars (star-empty, star-half, star-filled) now have fully sharp corners instead of barely perceptibly visible rounded corners. This was necessary to convert it to strokes.

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

Flaky tests detected in 932a439.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/27336025003
📝 Reported issues:

@jameskoster

Copy link
Copy Markdown
Contributor

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?

@jasmussen

Copy link
Copy Markdown
Contributor Author

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.

@jameskoster

Copy link
Copy Markdown
Contributor

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 fill styles. This can have a very different effect when the icons are outlined. We might need to swap some of those fill styles to use stroke instead. Probably worth smoke testing.

@jasmussen jasmussen requested review from a team and ajitbohra as code owners May 29, 2026 13:42
@github-actions github-actions Bot added [Package] Components /packages/components [Package] Edit Site /packages/edit-site labels May 29, 2026
@jasmussen jasmussen force-pushed the update/redraw-34-icons-to-stroke-based branch from d336873 to 9c97f83 Compare May 29, 2026 13:42
@jasmussen

jasmussen commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

It's funny you should mention that, because in testing other things, I found issues with exactly that:

Skærmbillede 2026-05-29 kl  15 32 20

This is now fixed, but I will write some additional detail on the fix in context of the code.

image


svg,
path {
svg {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work well

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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' } }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule overrode the SVG that was inserted, and forced a fill rule on it, making it impossible to use stroke-based icons.

Comment thread packages/icons/lib/generate-library.cjs Outdated
return `${ camelKey }: '${ value }'`;
} )
.join( ', ' );
return ` style={ { ${ declarations } } }`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

Size Change: +1.43 kB (+0.02%)

Total Size: 8.59 MB

📦 View Changed
Filename Size Change
build/modules/boot/index.min.js 52.7 kB +21 B (+0.04%)
build/modules/content-types/index.min.js 159 kB +111 B (+0.07%)
build/modules/edit-site-init/index.min.js 1.48 kB +83 B (+5.93%) 🔍
build/modules/workflow/index.min.js 19.9 kB +14 B (+0.07%)
build/scripts/block-directory/index.min.js 43.6 kB -94 B (-0.22%)
build/scripts/block-editor/index.min.js 380 kB +76 B (+0.02%)
build/scripts/block-library/index.min.js 323 kB -19 B (-0.01%)
build/scripts/commands/index.min.js 21 kB +11 B (+0.05%)
build/scripts/components/index.min.js 264 kB -21 B (-0.01%)
build/scripts/core-commands/index.min.js 4.41 kB +74 B (+1.71%)
build/scripts/edit-site/index.min.js 298 kB +977 B (+0.33%)
build/scripts/edit-widgets/index.min.js 22.1 kB -53 B (-0.24%)
build/scripts/editor/index.min.js 467 kB -5 B (0%)
build/scripts/format-library/index.min.js 30.9 kB +106 B (+0.34%)
build/scripts/media-utils/index.min.js 114 kB +129 B (+0.11%)
build/scripts/preferences/index.min.js 3.31 kB +15 B (+0.46%)

compressed-size-action

Comment thread packages/components/CHANGELOG.md Outdated
@@ -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">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Icon library, red context Icon library, red context

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:

Icon library using the Icon primitive from wp-ui

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

jasmussen added a commit that referenced this pull request Jun 11, 2026
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>
@jasmussen jasmussen force-pushed the update/redraw-34-icons-to-stroke-based branch from 932a439 to 19760b5 Compare June 11, 2026 09:40
@jasmussen

Copy link
Copy Markdown
Contributor Author

@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?

@jasmussen

Copy link
Copy Markdown
Contributor Author

The easiest interim thing we could do for the Icon block, is to explicitly turn off the stroke scaling effect, using vector-effect: none;:

Skærmbillede 2026-06-11 kl  11 47 37

This would allow me to complete the conversion of the set to be stroke-based, and then the value can be removed alongside the addition of a stroke-slider control. What do you think?

@jasmussen

Copy link
Copy Markdown
Contributor Author

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:

.wp-block-icon svg :where(path, rect, circle, ellipse, line, polygon, polyline) {
	vector-effect: none;
}

@jasmussen

Copy link
Copy Markdown
Contributor Author

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.

Comment thread lib/class-wp-icons-registry-gutenberg.php Outdated
Comment thread packages/block-library/src/icon/index.php Outdated
Comment thread packages/icons/lib/generate-library.cjs Outdated
Comment thread packages/icons/lib/generate-library.cjs Outdated
Comment on lines +320 to +322
// 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( ':' );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the suggestions. I've tried to apply them, and address the feedback here. Let me know if I got this right.

jasmussen and others added 18 commits June 12, 2026 12:11
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>
@jasmussen jasmussen force-pushed the update/redraw-34-icons-to-stroke-based branch from ed03e5d to 2e2bdf5 Compare June 12, 2026 10:11
Comment on lines +221 to +242
// `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 );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those changes may need tests

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

Labels

[Feature] Icons Related to Icon registration API and Icon REST API [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Package] Components /packages/components [Package] Edit Site /packages/edit-site [Package] Icons /packages/icons [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants