Migrating from T2 Icon to Core Icon
WordPress 7.1 introduced a public icon API (wp_register_icon(), wp_register_icon_collection(), wp_get_icon()) along with a core core/icon block. Since this covers the same use case as the T2 icons-package and the t2/icon block, the t2/icon block is deprecated as of version 8.32.0 for sites running WordPress 7.1 or newer.
Only the t2/icon block itself is deprecated. The T2 icons-package (T2\Icons\get_icon(), the t2_icons-filter, etc.) and its editor components — IconSelector, BlockIconSelector, the Icon component, and any block relying on them for icon controls — are not deprecated. They're still the mechanism T2 uses for icons everywhere outside of the standalone Icon block, and will keep being used until those places can be migrated to core's icon API as well.
Why the T2 icons-package isn't fully replaced yet
It's currently not possible to replace the T2 icons-package wholesale with core's icon API. Core's own icon picker (CustomInserterModal/IconGrid) lives inside @wordpress/block-library's private icon block folder and is only used by that block's own edit.jsx — it isn't exported from @wordpress/block-editor or @wordpress/components, so there's no public component T2 can reuse for IconSelector/BlockIconSelector. Building an equivalent picker ourselves against the /wp/v2/icons and /wp/v2/icon-collections REST endpoints is possible, but is a separate piece of work and hasn't been done yet.
This is why the migration in this guide is scoped to the t2/icon block only. Once core exposes (or T2 builds) a reusable icon-picking component, the rest of the icons-package and its editor components can be revisited.
This guide covers two parts of the migration:
- Registering your existing custom icons with core instead of (or alongside) the
t2_icons-filter. - Replacing existing
t2/iconblocks in content withcore/iconblocks.
1. Registering icons with core
Core icons are grouped in collections. Register a collection for your project, then register each icon into it on the init hook.
add_action( 'init', 'guides_register_icon_collection' );
function guides_register_icon_collection(): void {
wp_register_icon_collection(
'guides',
[
'label' => __( 'Guides icons', 't2' ),
]
);
}
T2 icons are stored as bare <path>/<polygon> markup (see the Working with icons guide), while wp_register_icon() expects a complete <svg> element. When migrating an existing icon, wrap the T2 markup with an <svg> tag and the same viewBox T2 used for it (T2 falls back to '0 0 24 24' when no custom viewbox is registered via t2_icon_viewboxes).
add_action( 'init', 'guides_register_icons', 20 );
function guides_register_icons(): void {
wp_register_icon(
'guides/icon-slug',
[
'label' => __( 'Icon slug', 't2' ),
'content' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.09 18.71a.996.996 5.3a.996.996 0 01-1.41 0z"/></svg>',
]
);
}
Core runs registered SVG content through sanitize_icon_content() before storing it, and as of WordPress 7.0/7.1 that function only allows <svg>, <path> and <polygon> elements (each limited to a fixed set of attributes) — everything else, including <circle>, <rect>, <g>, <use>, inline styles, stroke, and fill on the outer <svg>, is stripped. Most T2 icons that only use <path> shapes migrate cleanly, but check any icon built with other SVG elements — it'll need to be redrawn as path/polygon shapes, or left on t2/icon for now. There's no filter to extend this allowlist yet, though it's an active area of work upstream, so a future WordPress release may relax it.
If you have many icons registered through the t2_icons-filter, loop over T2\Icons\get_icons() and T2\Icons\get_icon_viewboxes() instead of copying each one by hand:
use function T2\Icons\{ get_icons, get_icon_viewboxes };
add_action( 'init', 'guides_register_icons_from_t2', 20 );
function guides_register_icons_from_t2(): void {
$viewboxes = get_icon_viewboxes();
foreach ( get_icons() as $slug => $markup ) {
wp_register_icon(
"guides/$slug",
[
'label' => $slug,
'content' => sprintf(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="%s">%s</svg>',
$viewboxes[ $slug ] ?? '0 0 24 24',
$markup
),
]
);
}
}
Give registered icons proper translated labels where possible, since the label is what's shown in the Icon block's picker and returned from the REST API.
2. Replacing t2/icon blocks with core/icon
The two blocks don't share an attribute schema, so there's no automatic block deprecation/transform path between them; existing t2/icon blocks in content need to be rewritten. The attributes map roughly like this:
t2/icon attribute |
core/icon equivalent |
|---|---|
icon (T2 icon slug, e.g. person) |
icon (namespaced, e.g. guides/person, after registering it per step 1) |
size (number, default 24) |
style.dimensions.width / style.dimensions.height (the dimensions block support) |
attrs['aria-label'] |
the block's ariaLabel support |
iconAttrs, attrs (other custom SVG/wrapper attributes) |
not supported — drop, or keep the icon on t2/icon |
tagName |
not supported — core/icon always renders its own wrapper element |
Because of the slug and size format differences, migrate content with a small WP-CLI/PHP script that walks blocks with parse_blocks()/serialize_blocks() rather than a find-and-replace. A minimal example, run once as a one-off (e.g. via wp eval-file):
<?php
// migrate-icon-blocks.php — run with: wp eval-file migrate-icon-blocks.php
// Map T2 icon slugs used in content to their new core icon names.
$icon_slug_map = [
'person' => 'guides/person',
'arrowForward' => 'guides/arrow-forward',
];
function guides_migrate_icon_block( array $block, array $icon_slug_map ): array {
if ( 't2/icon' === $block['blockName'] ) {
$attrs = $block['attrs'] ?? [];
$t2_icon = $attrs['icon'] ?? '';
$new_icon = $icon_slug_map[ $t2_icon ] ?? null;
if ( null === $new_icon ) {
// No mapping registered for this icon — leave the t2/icon block as is.
return $block;
}
$block['blockName'] = 'core/icon';
$block['attrs'] = [ 'icon' => $new_icon ];
if ( ! empty( $attrs['size'] ) ) {
$block['attrs']['style']['dimensions'] = [
'width' => $attrs['size'] . 'px',
'height' => $attrs['size'] . 'px',
];
}
if ( ! empty( $attrs['attrs']['aria-label'] ) ) {
$block['attrs']['ariaLabel'] = $attrs['attrs']['aria-label'];
}
$block['innerHTML'] = '';
$block['innerContent'] = [ null ];
}
if ( ! empty( $block['innerBlocks'] ) ) {
$block['innerBlocks'] = array_map(
fn( $inner_block ) => guides_migrate_icon_block( $inner_block, $icon_slug_map ),
$block['innerBlocks']
);
}
return $block;
}
$posts = get_posts( [
'post_type' => 'any',
'posts_per_page' => -1,
's' => 'wp:t2/icon',
] );
foreach ( $posts as $post ) {
$blocks = parse_blocks( $post->post_content );
$blocks = array_map(
fn( $block ) => guides_migrate_icon_block( $block, $icon_slug_map ),
$blocks
);
$new_content = serialize_blocks( $blocks );
if ( $new_content !== $post->post_content ) {
wp_update_post( [
'ID' => $post->ID,
'post_content' => $new_content,
] );
WP_CLI::log( "Migrated icon blocks in post {$post->ID}" );
}
}
Before running this against production content:
- Take a database backup.
- Run it against a staging copy first and diff a few affected posts in the editor.
- Extend
$icon_slug_mapto cover every T2 icon slug actually used in your content — you can find them by searching the database forwp:t2/iconoccurrences (as the script above does viaget_posts( [ 's' => 'wp:t2/icon' ] )). - Icons that have no mapping are intentionally left as
t2/iconblocks rather than silently dropped, so you can find and fix them.
Once content has been migrated and no t2/icon blocks remain, the t2/icon block can be excluded for sites on WordPress 7.1+ by adding "exclude-blocks": ["t2/icon"] to your theme's t2.json.