Icons are a language shared by Core blocks, themes and plugins — notes from building one outside Core #82830
Replies: 3 comments
|
Font family metadata — letting a font family declare its intended use, such as an icon font — is now tracked separately in #82848. |
|
One related data-model question: if variable font axes are going to be exposed to block UI, I think it may be worth considering a structured representation rather than treating "fontVariationSettings" only as a raw CSS string. Today theme.json represents it as a string: That works well as a CSS serialization format, but it is awkward for consumers such as Paragraph or Icon controls that need to read and update individual axes. A map/object would be easier to consume: The Style Engine / "WP_Font_Face" boundary could then serialize that to: This also seems related to Core Trac #66103. "WP_Font_Face" already has a structured PHP array code path for "font-variation-settings", but the current compiler produces invalid CSS for that input. That suggests there is already a partial structured representation in Core which has not been reconciled with the string-only theme.json schema. For UI this becomes especially useful with fonts such as Material Symbols: an Icon block could toggle "FILL", while Paragraph or other typography controls could independently manipulate "wght", "wdth", "opsz", or custom axes without parsing and rebuilding a CSS declaration string each time. For compatibility, the existing string form could remain accepted while a structured object becomes the canonical form for new UI. |
|
The variable font axis model is now proposed in #83148. It corrects the object example in my comment above: |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Gutenberg has no shared contract that connects icon sources, variable-font capabilities, and control state. Individual blocks can approximate it, but cannot interoperate or expose it consistently through themes, the Font Library, and the editor.
I reached that conclusion by building the pieces in order. First an icon block that takes its icon from either the Icon Registry or the theme's icon font. Then an icon button that renders that icon. Then copies of
core/buttonandcore/buttons, so a labelled button and a group of toggles could use the same icon. Each step ran into a question the previous one could not answer on its own: which fonts are icon fonts, which element an icon may be painted in, where a selected icon is stored, what state switches it. By the end it was clear this is not an Icon Registry problem alone. It is the icon language that Core blocks, themes and plugins all speak, with no shared grammar.That work is now inspectable rather than described:
This thread is an umbrella. Several of its questions already have their own threads, linked where they come up, and I would rather point at those than restate them here.
What exists today
Read from
trunk(edd6dcb):core/iconis dynamic and paints into adiv. It has nosave;index.phpreturnssprintf( '<div %s>%s</div>', $wrapper_attributes, $svg ). Its attributes areicon,flipHorizontal,flipVerticalandrotation.core/buttonis already a hybrid, and polymorphic. It hassave.jsxandrender_callback => render_block_core_button, which walks the saved markup withWP_HTML_Tag_Processor. ItstagNameattribute isa(default) orbutton.FONT_FAMILY_SCHEMAacceptsfontFamily,name,slugandfontFace; each face acceptsfontVariationSettings. Nothing marks a family as icons.lib/block-supports/typography.phphandles font family, size, style, weight, letter spacing, line height, text align, columns, decoration, transform, indent and shadow. A theme can declare"FILL" 0, "wght" 400on a face; no block, including Paragraph, can move an axis without custom CSS.core/buttonsis a layout container. It declares no attributes and a fixed flex layout. Nothing in it relates its buttons to each other — no selection, no shared state.WP_Icons_Registry::register()allowslabel,contentandfile_path(detail in Define a state-aware icon reference contract for blocks #82700).core/iconasks the registry. Inpackages/block-library,icon/index.phpis the one block that callswp_get_icon(); other blocks draw their UI icons inline. Resolve SVG through Icon Registry #82062 (open) proposes resolving those through the registry under stable identifiers, so a theme can replace one by re-registering an SVG. That helps a theme whose icon language is SVG; a theme whose icon language is a font would still have to convert each glyph to markup — the language spans more than the registry.What the plugin does differently, and what it cost
The plugin has a Dialog Icon, a Dialog Button, a Dialog Icon Button and a Dialog Button Group. They share one icon renderer for both sources, so the findings below are about the contract, not about one block.
One paint element for both sources. The icon is always
span.ax-icon: a glyph for a font,span > svgfor the registry. Size (--md-icon-size), colour, flip/rotate and the accessible label all land on the span, so no rule branches on the source.The
spanis also forced by the button. A<button>accepts phrasing content only, sobutton > div > svgis invalid, whileais transparent anda > divcan be valid. A block that renders as either element —core/button'stagNameisaorbutton— needs inner markup valid in both, and that common subset is phrasing content. If an icon wrapper may be adiv, the icon has to know its parent'stagName, or grow an inline rendering mode. Fixing it tospanremoves the question: the same icon markup is valid in a link, a button, a heading or a paragraph.Selection needs the button, not the icon. A variable font fills the same glyph (
FILL 0 → 1, interpolated because the axis is a registered custom property); a static font or the registry needs a second reference, rendered beside the first and chosen byaria-pressed. The icon primitive knows nothing of state — the storage half of this is Define a state-aware icon reference contract for blocks #82700.State can be previewed and animated without new runtime. Hover and focus can show the selected state before it is chosen (a Like filling, a Repost turning), and the swap can fade, rotate or scale. A rotate-only transition turns a single icon — a plus to a close at 45°, a chevron at 180° — which is the case
core/accordion-headinghandles today with a literal+and a CSS rotation.A button that opens something is stateful too. A trigger whose surface is open carries
aria-expanded, and the same selected icon applies.In the VQA page: the "Icons" row puts
core/iconbeside both sources; "Icon states" has Like (fill), Repost (rotate-only), Expand (registry swap, fade) with hover preview; "Selection" has single, required and multiple groups.Proposed work, separable
Each of these can move without the others.
1. Theme: declare an icon font as an icon font.
A family-level flag (for example
icon: true, default false) that the Font Library and editor can read, so an icon picker can offer the theme's icon font without a plugin hard-coding its class and ligature names. Deliberately not a glyph map: Material Symbols works by ligature and by codepoint, and enumerating glyphs is the cost #82229 asks to avoid. #82229 left a schema slot out on purpose, to keep the provider question narrow; this is the layer that thread declined to design, raised here on its own.2.
core/icon: one source contract,spanpaint element.Let the block reference either a registry icon or a theme icon-font glyph, stored explicitly (
iconSource), with the same flip/rotate/label behaviour on both. Considerspanas the painted element. The block wrapper matters as much as the painted element: nested inside a<button>as an inner block, today'sdivwrapper would be invalid however the icon itself is painted, so a control should either call the icon renderer directly and place only itsspan, or the block's wrapper should be aspantoo.3. Variable-font axes as a typography capability.
Not an icon-only inspector. If a face declares axes, blocks that support typography could expose them — Paragraph and Heading as much as Icon. Icons are simply where the gap is most visible, because
FILLis how an icon font expresses selection.4. Button state primitives.
Selected icon (#82700 for storage), fill or weight fallback ("If a filled version doesn't exist, increase the weight instead"), hover/focus preview, icon transitions, and rendering from
aria-pressed/aria-expanded— including state held by a plugin's Interactivity store, as a Like or Repost button's is. Buttons are the natural first consumer because a toggle is where M3 specifies all of this, which is also why an icon proposal ends up improving the button's state model.5.
core/buttonsand a button group are probably different things.A hypothesis, not a request to change
core/buttons: selection rules, required selection and shared state make a group an interaction container, whilecore/buttonsis layout. Whether that is a new block, a support, or nothing, is a question for after the cases above are visible.A personal position on
core/buttonPersonally, I would propose treating the current
core/buttonas a legacy (deprecated) version and moving the block to server rendering. The goal is one renderer and one state contract shared with an icon button: icon resolution, selected icons andaria-pressedwritten at render time rather than frozen in saved HTML.The layering is what leads there — a logical layering, not nested blocks. The icon is a primitive; an icon button is that primitive rendered in a button shell, with its name out of sight; a button is the same shell with an icon and a visible label. None of them contains another as an inner block: they share one renderer.
core/iconalready resolves its icon on the server. For the icon button and the button to place that same resolvedspan— and switch it by state — they need the same server step, not one block rendering at request time while the other replays saved HTML.The plugin is a working reference for testing these API decisions, not code for Core to adopt. Dialog Icon, Dialog Icon Button and Dialog Button call one icon renderer, in the editor and on the page, and the VQA page shows that:
aorbutton;Whether this can be built is answered by the page. The question for Gutenberg is how to generalise the contract into Core APIs.
I considered the two ways that keep saved markup:
WP_HTML_Tag_Processorchanges attributes, not an element's contents, so this becomes string surgery beside the existing pass, and a button and an icon button keep separate render paths.core/iconis dynamic — and state markup is still tied to saved HTML.Core's own blocks point the same way. On
trunk(edd6dcb), no block inpackages/block-librarystores inline SVG in saved markup: nosaveordeprecatedfile contains<svg,<SVG,<Pathor@wordpress/primitives. Every inline SVG is written at render time:search,social-link,page-list,navigation-overlay-close,iconsaveindex.php(iconviawp_get_icon())navigation,navigation-link,navigation-submenunavigation/index.php,navigation-link/shared/render-submenu-icon.phpimagefigure > img, plus arender_callbackimage/index.phpThe two exceptions to "dynamic" are instructive.
core/accordion-headingis fully static and has a toggle icon, and it saves a literal+turned by CSS rather than an SVG.core/imagekeeps static markup and adds an SVG on the server, and it does so by string replacement:preg_match( '/<img[^>]+>/', … ), thenpreg_replace()with the image followed by a<button>holding the SVG — because the tag processor cannot insert content. A button's icon has to go inside the control rather than beside it, so the same approach would be harder forcore/button, not easier.I recognise the cost: the number of saved buttons, consumers that read
post_contentdirectly, and render overhead.core/buttonalready runs arender_callbackover its saved markup, so this extends an existing server path rather than introducing one. The plugin's buttons are fully dynamic, but that is an experiment's choice; I am offering it as a position to argue with, not as the conclusion.Non-goals
This proposal intentionally does not introduce alternate button labels for state changes. Label changes alter the accessible-name contract and need a separate stateful-command model, including product-owned localization for verbs such as Like/Unlike or Follow/Unfollow. "Stateful button labels" is a possible follow-up, and closer to the button's accessibility and string contract than to the icon API.
Also out of scope: changing the Icon Registry's registration shape (#82700 explains why it can stay), and uploadable site icons.
Related
core/button; an earlier comment there gives the icon-only button markup this builds oniconthrough the widget pipeline #80938 — a declarativeiconin the widget pipelineAll reactions