WordPress 7.1 lets you register icons. Then it deletes your strokes.

Hand-drawn whiteboard diagram: a line-drawn clock SVG passes through WordPress's wp_kses icon allowlist, which permits only svg, path and polygon and no stroke attributes, and comes out the other side as a solid black disc because the shapes fall back to the default black fill. Stroke-drawn icon sets fail; fill-drawn sets pass.

WordPress 7.1 opens the icon picker to plugins. You register a collection, your icons get their own tab next to core’s, and the whole thing renders server-side from a name instead of storing SVG in your content. It is a genuinely good API, and I was happy to use it.

Then I opened the picker and half my icons were solid black lozenges.

Not all of them. A clock came through as a filled black disc with no hands. A phone was a black blob roughly the shape of a phone. But a heart looked perfect, and so did a folder. Same collection, same registration call, wildly different results, with nothing in my code to explain the split.

Here is the mechanism, read from WordPress 7.1. When you call wp_register_icon(), the SVG does not go into the registry as written. It first passes through sanitize_icon_content() in wp-includes/class-wp-icons-registry.php, which runs it through wp_kses against a hardcoded allowlist. The docblock is honest about its heritage: “Logic borrowed from twentytwenty.” Here is the allowlist, verbatim:

$allowed_tags = array(
    'svg'     => array( 'class', 'xmlns', 'width', 'height',
                        'viewbox', 'aria-hidden', 'role', 'focusable' ),
    'path'    => array( 'fill', 'fill-rule', 'd', 'transform' ),
    'polygon' => array( 'fill', 'fill-rule', 'points', 'transform', 'focusable' ),
);
// (attribute values collapsed to keys for readability)

Three elements survive: svg, path, polygon. Everything else is deleted. No circle, no rect, no line, no g. And look at what is missing from the attribute lists: there is no stroke anywhere, and the outer <svg> is not allowed a fill. Anything the allowlist does not name, wp_kses strips silently, no warning, no error in your log.

That single detail is the whole story, and it explains the split I was seeing.

A stroke-drawn icon is built the opposite way from how it looks. The visible line is not a filled shape; it is a thin path with stroke="currentColor" and no fill, and the “no fill” almost always comes from one fill="none" on the outer <svg> that every shape inherits. The sanitizer takes both of those away. It strips stroke from the shapes, and it strips fill from the <svg>. Now nothing tells the shape what to do, so it falls back to the SVG default, which is fill: black. A hairline outline becomes a solid silhouette. The clock’s ring, meant to be a stroked circle, fills in and swallows the hands, which were themselves stroked <line> elements that got deleted outright.

The icons that survived were the ones drawn the other way. A fill-drawn icon makes its shape out of filled paths, and its outline look comes from the d attribute tracing a thin closed region, not from a stroke. Nothing in the allowlist touches those. path, fill, d: all permitted. So a fill-drawn heart passes through untouched while a stroke-drawn clock arrives as a blob.

That is the part worth carrying away, because the obvious lesson is the wrong one. The trap is not “outline icons don’t work.” Plenty of outline-styled sets render perfectly. The trap is strokes, specifically. Phosphor, Bootstrap Icons, and Material Symbols draw their outlines as filled paths and pass clean. Tabler, Feather, and Lucide draw theirs with strokes and cannot survive the sanitizer no matter what you do to the markup, because the property that carries the whole design is the one property the allowlist removes.

I found this out the honest way, by trying a Tabler-based collection and looking at it. That detour was the discovery. The stripping is invisible on a fill-drawn set, because there is no stroke to lose. Had I started on Phosphor, every icon would have rendered perfectly and I would never have known the allowlist was there. The fix was not a clever escape; there isn’t one, since you don’t control wp_kses. The fix was to rebuild the collection on Phosphor, drawn as filled paths that pass through untouched. Color, at least, takes care of itself. Core’s Icon block sets fill: currentColor on the svg in its own CSS, so even with the <svg>‘s fill stripped, a bare path inherits the surrounding text color. The only time you add fill="currentColor" yourself is when you render an icon outside the block.

None of this is hidden, for the record. The official dev note for 7.1 says the allowlist “doesn’t permit stroke anywhere, so a stroke-based icon loses its stroke,” and it flags that the list may be widened in a future release. But documented and intuitive are different things, and “your icon set’s entire drawing method is silently unsupported” is the kind of sentence that reads as trivia right up until it is your afternoon.

The part that transfers past WordPress is narrower than it looks. A sanitizer’s allowlist, not your CSS, silently decides which icons survive, and almost nobody reads it until something breaks. WordPress’s allowlist happens to drop stroke. Another platform’s might allow it and drop something else, or nothing at all. The rule is not “strokes die everywhere.” It is “find the allowlist and hold your set up against it before you commit.” For WordPress 7.1, that check takes one line. Open an icon file and search for stroke. If it’s there, keep shopping.

There is a small comedy in the gatekeeper. The rulebook for WordPress’s 2026 icon API is, by the code’s own admission, borrowed from Twenty Twenty, a default theme from 2020. Six years on, it may yet learn about strokes.