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.

link 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:

  1. Registering your existing custom icons with core instead of (or alongside) the t2_icons-filter.
  2. Replacing existing t2/icon blocks in content with core/icon blocks.

link 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.

link 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:

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.