Skip to content
System nominalUTC --:--:--Articles 080
MODULE 03 Lesson 09 / 18 35 MIN

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.

SupportAttributes addedMarkup 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.

Lab 40 minutes

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.