Building blocks
Block Supports and the Editor UI
What each supports key switches on, which ones write markup and which only change the editor, and why a support declared at the wrong moment does nothing at all.
You add "supports": { "anchor": true } to the manifest, reload the editor,
and there is still no way to give the block an id to link to. You then add
"color": { "background": true } and the background control appears, so the
support system works and something about the first one does not.
Both of those are the same system, and the difference between them is the thing to internalise: a support is a declaration that WordPress reads to add attributes and to generate markup, and half the keys never touch a file you wrote. Understanding which half does what is what lets you predict the output instead of iterating in the browser.
A support declares attributes and generates markup
Every support does one or both of two jobs. It registers attributes on the block type at
registration time, and it contributes wrapper attributes when the block is rendered. The
anchor support is the clearest example, because its source is short enough to read in full:
if the support is on and the anchor attribute is a non-empty string, it returns
array( 'id' => $value ). The colour support does more, but the shape is the
same.
The attributes are registered by a hook, not by the support file as it loads.
WP_Block_Supports::init() runs on init at priority 22, walks every
block type registered so far, and calls each support's registration callback. That timing is
the whole explanation for the second half of this lesson.
A block registered after init priority 22 never gets its support attributes
Register the same block at two different priorities and compare the attribute lists. A
block registered at priority 5, before the supports walk, comes back with
align, className, anchor and style. The
identical registration at priority 30 comes back with only lock and
metadata, which are the two global attributes every block type gets in its
constructor.
m3probe2/early attributes=["lock","metadata","align","className","anchor","style"]
m3probe2/late attributes=["lock","metadata"]
This is the failure mode behind a support that never shows up in the editor. The attribute
is missing, so the control has nothing to write to, and the support's own generator still
runs at render time because it re-checks block_type->supports rather than the
attributes. You end up with markup that can carry an anchor and no way to set one.
The other consequence is validation. prepare_attributes_for_render() only
validates and defaults attributes that are declared on the block type, so anything a support
would have declared is passed through unexamined. Registering on init at the
default priority of 10 is early enough. Anything at 22 or later is not, and neither is
registering on a later hook such as wp_loaded or rest_api_init.
Some supports write markup, and some only move things around
The keys people reach for first are the visual ones, and they split cleanly. Colour,
spacing, typography, border, dimensions, shadow and background contribute classes and inline
styles. Alignment contributes a class, and only for the values the theme allows, which is
why align: [ "wide", "full" ] and align: true produce different
things. Anchor contributes the id. Those are the ones whose result you can find
in the saved markup.
Visibility belongs in that group as well, and it is easy to file it wrongly because it
looks like a piece of editor chrome. Since 6.9 it writes media queries for the viewports a
block is hidden on, and a stored metadata.blockVisibility of
false removes the block from the rendered HTML outright, whether or not the
block type declares the support. Turning the support off does not bring that content
back.
The rest are editor concerns. html: false removes the "Edit as HTML" entry.
inserter: false hides the block from the inserter and the transforms menu
without preventing it from being inserted programmatically. multiple: false
allows one instance per post. lock, renaming,
contentRole, listView, splitting,
reusable, alignWide and customClassName are all of
this kind. None of them changes a byte of front-end output.
| Support | Attributes added | Markup it generates |
|---|---|---|
align |
align |
Alignment class on the wrapper |
anchor |
anchor |
id on the wrapper |
color |
backgroundColor, textColor, gradient, style |
Preset classes and inline colours |
spacing |
style |
padding, margin and gap declarations |
typography |
fontSize, fontFamily, style |
Font size class or declaration, line height, weight |
visibility |
None of its own | Media queries, or no output at all when hidden |
html: false |
None | None. Editor only. |
multiple: false |
None | None. Editor only. |
inserter: false |
None | None. Editor only. |
Two of these are worth reading twice. style is an attribute for colour,
spacing and typography alike, because all three write into the same object. Declare your own
style attribute in the manifest and you are taking over a key the support system
also uses; a support that declares it later leaves your definition alone only because it
checks whether the key already exists.
customClassName is not the same as className.
className controls the generated wp-block-your-name class, and
turning it off is a decision about whether your block can be styled from the outside at all.
customClassName controls the additional classes field in the advanced panel.
When the markup gets written depends on the block, not the support
A support's generator produces wrapper attributes. Where they land depends on whether the
block has something to render on the server. A block with a render callback, whether that
came from the manifest's render key or from a callback in your arguments, calls
get_block_wrapper_attributes() inside that callback, so the classes and inline
styles are computed on every request, after the block has been parsed and its attributes
prepared. A static block has no server-side step at all. The editor's
useBlockProps.save() carries the support-generated classes and styles into the
element that gets saved, and from then on they are part of the post content.
That difference decides where you look when a support appears not to work. For a block with a render callback, dump the rendered HTML and the attribute array at the callback. For a static block, read the post content out of the database, because the front end is not generating anything. Testing a static block by inspecting a server render tells you nothing, and changing a support's generation on the PHP side does not retroactively change markup that was saved years ago.
There are two implementations of the same rules, which is worth remembering when the
two outputs differ. The editor builds the wrapper in JavaScript and PHP builds it in
get_block_wrapper_attributes(). They agree only if you used
useBlockProps.save(). Write your own wrapper markup in save and the
block can be perfectly valid while the two disagree.
A support also generates its classes whether or not the theme defines a matching rule.
Set a font size to a preset slug your theme.json does not declare and the markup
still carries has-large-font-size. The class is there, the rule is not, and the
text renders at the inherited size.
BlockControls and InspectorControls are for your own attributes
Supports cover the controls WordPress already knows how to build. Everything else comes
from the two React components that place UI in the editor: BlockControls fills
the block toolbar, and InspectorControls fills panels in the settings sidebar.
Both take the block's setAttributes and read from attributes, and
both are rendered from edit rather than declared anywhere.
import { InspectorControls, BlockControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, ToolbarButton } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
return (
<>
<BlockControls>
<ToolbarButton
icon="star-filled"
isPressed={ attributes.featured }
onClick={ () => setAttributes( { featured: ! attributes.featured } ) }
>
{ __( 'Featured', 'mytheme' ) }
</ToolbarButton>
</BlockControls>
<InspectorControls>
<PanelBody title={ __( 'Settings', 'mytheme' ) }>
<ToggleControl
label={ __( 'Show the byline', 'mytheme' ) }
checked={ attributes.byline }
onChange={ ( byline ) => setAttributes( { byline } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>{ attributes.text }</div>
</>
);
}
The featured and byline attributes in that snippet have to be
declared in the manifest like any other, or the editor will not persist them. An inspector
control that writes an undeclared attribute saves fine on the client and loses the value on
reload, which is a genuinely confusing bug to chase because the toggle appears to work.
The theme.json palette is the palette the block offers
Nothing in the block decides which colours a user can pick. The editor's colour settings
come from get_block_editor_settings(), which reads
wp_get_global_settings(), which is the merge of core defaults, the theme's
theme.json and any user changes. One line of that function is the rule worth
remembering: the editor's colors value is whichever of the user, theme or
default palette exists first in that order.
So a block with "color": { "background": true } on a site running Twenty
Twenty-Five is offered Base, Contrast and Accent 1 through 6, because those are the eight
slugs that theme declares. The block contributes permission, not contents.
The custom hex field is the other half of that. It is controlled by
settings.color.custom in theme.json, and it defaults to allowed.
Setting it to false removes the picker, and the same pattern applies to
settings.color.customGradient, settings.spacing.custom and
settings.typography.customFontSize. A block that declares colour support on a
site with custom colours disabled offers the palette and nothing else, and that is the
theme's decision rather than the block's.
Take the block from the previous lesson and declare four supports:
align with an explicit list, anchor, color with
background and text but no gradient, and spacing with padding only.
Register it once at init priority 5 and once at priority 30, under two
names, and dump the attribute keys for both from the command line. Explain the
difference in terms of what runs at priority 22.
For the early registration, render a block with an alignment value, a preset
background slug, custom text colour, and padding, and write down which of the four values
appears as a class, which as an inline style, and which as an attribute on the wrapper
element. Then set settings.color.custom to false in
theme.json and describe exactly what changes in the editor and what changes
in the markup.
Finally, add a InspectorControls panel toggling one boolean attribute of
your own, and confirm the value survives saving the post and reloading it.
Show the solution and the reasoning
The late registration comes back with lock and metadata, the
two global attributes. The early one has those plus align,
className, anchor, style,
backgroundColor, textColor and fontSize, depending
on which supports were declared. There is no error and no notice in either case, because
nothing failed. The supports walk never saw the block.
Successful registration on a dynamic block produces markup in this shape. The exact order of the classes is not something to depend on, and the fluid font size is the style engine's clamp calculation rather than the value in the attribute:
<div
style="color:#ff0000;font-size:clamp(0.984rem, 0.984rem + ((1vw - 0.2rem) * 0.809), 1.5rem);padding-top:2rem"
class="alignwide wp-block-mytheme-note has-text-color has-background has-accent-1-background-color"
id="section-two">
</div>
The preset background slug becomes a class and a marker class, and the value itself
stays in theme.json. The custom text colour becomes an inline declaration
plus has-text-color. The padding value becomes an inline declaration. The
alignment and the anchor are attributes rather than styles. That split is the practical
summary of the visual supports: slugs become classes, free values become inline styles,
and alignment and identity become attributes.
Switching settings.color.custom to false changes only the
editor. No stored block changes, no class disappears, no inline style is removed. The
picker for a custom colour is gone, so the only way to choose a colour is a palette slug,
and the next person to edit the block cannot introduce an off-palette value. The markup
from before the change stays exactly as it was, which is the trap: an existing custom
colour survives a setting that looks like it should remove one.
The inspector toggle needs the attribute declared in the manifest. A boolean with a
default is the safe shape, because a missing value then resolves to false
rather than undefined:
{
"attributes": {
"byline": { "type": "boolean", "default": true }
}
}
What supports cannot do is the limit worth naming. There is no support for a control that changes what an attribute means, no support that runs your own validation, and no support that adds a panel to another block. The list of keys is fixed in core, the registration of a key you invent is accepted by the REST schema and then ignored by everything, and the editor will not tell you that the key was misspelled. When a support appears to do nothing, check the attribute list on the block type before you check your manifest.
Progress
Mark this lesson done and it stays marked in this browser.