Skip to content

Icons API: Add wp_get_icon() to render registered icons as inline SVG#12010

Open
t-hamano wants to merge 4 commits into
WordPress:trunkfrom
t-hamano:64847-wp-icon-function
Open

Icons API: Add wp_get_icon() to render registered icons as inline SVG#12010
t-hamano wants to merge 4 commits into
WordPress:trunkfrom
t-hamano:64847-wp-icon-function

Conversation

@t-hamano

@t-hamano t-hamano commented May 29, 2026

Copy link
Copy Markdown
Contributor

This PR adds wp_get_icon(), a procedural wrapper around WP_Icons_Registry that returns a registered icon as an inline SVG string.

Based on WordPress/gutenberg#78332.

Trac ticket: https://core.trac.wordpress.org/ticket/64847

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Porting the function and PHPUnit tests from the referenced Gutenberg PR to core conventions. I reviewed and tested the result; all 9 tests pass.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Introduce a procedural wrapper around `WP_Icons_Registry` so themes and
plugins can render any registered core icon as an inline SVG from PHP,
mirroring the React `<Icon>` component. This bridges the gap for PHP
contexts that otherwise depend on the deprecated dashicons font.

`wp_get_icon()` looks up the icon content via the registry and uses
`WP_HTML_Tag_Processor` to apply configurable `size` and `class`
attributes, plus accessibility handling: a `label` produces
`role="img"` and `aria-label`, while its absence marks the icon
`aria-hidden="true"`.

The function lives in a new `icons.php`, following the established
registry-class-plus-procedural-API pairing used by connectors.php and
abilities.php.

Props ...

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@t-hamano t-hamano marked this pull request as ready for review May 29, 2026 06:46
@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.

Core Committers: Use this line as a base for the props when committing in SVN:

Props wildworks, mukesh27, jonsurrell, tyxla.

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

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

The test file only covers wp_get_icon(); the_wp_icon() does not exist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/wp-includes/icons.php
* @return string SVG markup for the icon, or empty string if not found.
*/
function wp_get_icon( $name, $args = array() ) {
$icon = WP_Icons_Registry::get_instance()->get_registered_icon( $name );

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 think better to check if $name is not empty. WDYT?

if ( empty( $name ) ) {
	return;
}

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.

Since get_registered_icon() returns null if $name is empty, adding an empty check feels redundant.

@sirreal sirreal left a comment

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.

Reproducing some feedback that I unintentionally left elsewhere (WordPress/gutenberg#78332 (review)).

Comment thread src/wp-includes/icons.php
* @return string SVG markup for the icon, or empty string if not found.
*/
function wp_get_icon( $name, $args = array() ) {
$icon = WP_Icons_Registry::get_instance()->get_registered_icon( $name );

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.

What do we know about the registered icons? If this is a public API where arbitrary SVG content (that passes KSES checks) may be in the icon, things can get very complicated. One that comes to mind is if the SVG has a viewBox, then a single size argument may be tricky. Are icons required to be square? Is that enforced?

And just to confirm, anything in the registry has passed through KSES, right?

Comment thread src/wp-includes/icons.php
Comment on lines +56 to +58
if ( ! empty( $args['class'] ) ) {
$processor->add_class( $args['class'] );
}

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 this a single class, or could it be multiple classes? The ::add_class method expects a single class, and multiple classes should be applied individually.

Something like $processor->add_class( 'wp-icon class2 class3' ) is not how the method is intended to be used.

Comment thread src/wp-includes/icons.php
Comment on lines +60 to +65
if ( ! empty( $args['label'] ) ) {
$processor->set_attribute( 'role', 'img' );
$processor->set_attribute( 'aria-label', $args['label'] );
} else {
$processor->set_attribute( 'aria-hidden', 'true' );
}

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.

Consider that the svg content may already have these attributes, so if one branch should have role and aria-label but not aria-hidden, and vice-versa for the other branch, then it's a good idea to remove the attributes from the other branch so we don't wind up with:

<svg aria-hidden="true" role="img" aria-label="whoops?" />
Suggested change
if ( ! empty( $args['label'] ) ) {
$processor->set_attribute( 'role', 'img' );
$processor->set_attribute( 'aria-label', $args['label'] );
} else {
$processor->set_attribute( 'aria-hidden', 'true' );
}
if ( ! empty( $args['label'] ) ) {
$processor->set_attribute( 'role', 'img' );
$processor->set_attribute( 'aria-label', $args['label'] );
$processor->remove_attribute( 'aria-hidden' )
} else {
$processor->set_attribute( 'aria-hidden', 'true' );
$processor->remove_attribute( 'role' );
$processor->remove_attribute( 'aria-label' )
}

(Do check me and test this, consider the suggestion pseudo-code)

Comment thread src/wp-includes/icons.php
Comment on lines +52 to +53
$processor->set_attribute( 'width', (string) $args['size'] );
$processor->set_attribute( 'height', (string) $args['size'] );

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.

What if we get something other than int, is that possible? The string "10%", or other less sensical values: [ '9', '9' ], "nonsense".

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.

Yeah, came here to ask some of the same. Also, what about negative integers - should we make sure it's not the case by calling absint() on it or something?

Comment thread src/wp-includes/icons.php
$processor->set_attribute( 'role', 'img' );
$processor->set_attribute( 'aria-label', $args['label'] );
} else {
$processor->set_attribute( 'aria-hidden', 'true' );

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 decorative icons, should we also disable focusable to match what render_block_core_icon() is doing?

Suggested change
$processor->set_attribute( 'aria-hidden', 'true' );
$processor->set_attribute( 'aria-hidden', 'true' );
$processor->set_attribute( 'focusable', 'false' );

Comment thread src/wp-includes/icons.php

$processor->set_attribute( 'width', (string) $args['size'] );
$processor->set_attribute( 'height', (string) $args['size'] );
$processor->add_class( 'wp-icon' );

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.

How necessary is this class? Are we adding it anywhere else? I don't think we are, and this may mean we're actually producing non-identical output with render_block_core_icon() for example.

If we want to adopt it, we should likely consider adding it to render_block_core_icon() and any other React paths that render icons.

Comment thread src/wp-includes/icons.php
* }
* @return string SVG markup for the icon, or empty string if not found.
*/
function wp_get_icon( $name, $args = 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.

Not a blocker, but we may want to refactor render_block_core_icon() to use this function instead of reinventing some of the same logic.

Comment thread src/wp-includes/icons.php
$processor->set_attribute( 'aria-hidden', 'true' );
}

return $processor->get_updated_html();

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.

Just to clarify, are we intentional about not adding a filter here?

Comment thread src/wp-includes/icons.php
Comment on lines +52 to +53
$processor->set_attribute( 'width', (string) $args['size'] );
$processor->set_attribute( 'height', (string) $args['size'] );

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.

Yeah, came here to ask some of the same. Also, what about negative integers - should we make sure it's not the case by calling absint() on it or something?

*/
public function test_wp_get_icon_escapes_attributes() {
$output = wp_get_icon( 'core/plus', array( 'class' => '"><script>alert(1)</script>' ) );
$this->assertStringNotContainsString( '<script>', $output );

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.

What's the expected form here? &lt;script&gt;? Might make sense to add some more assertions or just change to a positive assertion that matches what exactly we expect.

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 could probably add some more assertion like assertEqualHTML for things like assertHTMLContainsNoTags( [ 'script' ], $html ) or similar.

For this case, it may be a good idea to create a tag processor with something like this, what we care about is not having a SCRIPT tag:

$p = new WP_HTML_Tag_Processor('<svg><script></script>');
while ( $p->next_token() ) {
  var_dump( $p->get_token_name() );
  if ( 'SCRIPT' === $p->get_token_name() ) {
    echo 'TEST FAILURE';
    throw new Error('bad bad bad');
  }
}
// assert $p->paused_at_incomplete_token() === false;

Or it may be better to just assert that that class was set:

$p = new WP_HTML_Tag_Processor('<hax0r>');
$p->next_tag();
$p->add_class('"><script>alert(1)</script>');
$malicious = $p->get_updated_html();

$p = new WP_HTML_Tag_Processor($malicious);
$p->next_tag();
assert( $p->has_class( '"><script>alert(1)</script>' ) );
echo 'test passed';

* @ticket 64847
*/
public function test_wp_get_icon_escapes_attributes() {
$output = wp_get_icon( 'core/plus', array( 'class' => '"><script>alert(1)</script>' ) );

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 also test escaping in label and aria-label and any other user-controller attribute?

Comment thread src/wp-includes/icons.php
$processor->set_attribute( 'height', (string) $args['size'] );
$processor->add_class( 'wp-icon' );

if ( ! empty( $args['class'] ) ) {

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 it fine if we pass multiple classes there (class-1 class-2)? To be precise and use the API correctly, should we explode() those and call add_class() for each of them? If you agree, let's also add a test for that.

@t-hamano

Copy link
Copy Markdown
Contributor Author

@sirreal @tyxla Thanks for the feedback! To avoid duplicate reviews, it might be better to focus reviews on the Gutenberg PR. Once the Gutenberg PR is ready for shipping, I'd like to reflect that code into this PR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants