Attributes, Supports and InnerBlocks
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This sub-section’s WordPress bibliography lists the reference material consulted while preparing these pages. |
A block is more than its markup: it has typed data (attributes), opt-in design tools that WordPress wires up for it
(supports), and possibly child blocks (InnerBlocks) that can read values from their parent (block context). This page
is for block developers. It explains each of those four mechanisms, what they store in the post content, what PHP sees
of them on the server and what HTML and CSS they produce on the front end, using a new lumen/book-shelf block of the
Lumen Press lumen-catalog plugin that holds lumen/book-card children. The block.json file itself and its
registration are described in block.json and Registration;
edit, save, validation and render.php in Static and Dynamic Blocks;
the build tooling in Block Development Environment.
This page was written against WordPress 7.1.3 (released 2026-10-06; 7.1.4, a security release of 2026-10-28, is the
version to run in production), @wordpress/scripts 37.0.0, block.json schema https://schemas.wp.org/wp/7.1/block.json
and the Gutenberg plugin 24.2.0, and checked on 2026-10-28. WordPress 7.2 is
in beta and due in December 2026; the Block Editor Handbook already documents a few 7.2 supports, which are flagged
below as not available in 7.1.
The example: a book shelf
lumen/book-shelf is a static block: a heading, a short introduction, a row of badges ("Signed copies"), a grid of
lumen/book-card blocks and a link to the catalogue. It lives next to the other blocks of the plugin, in
lumen-catalog/src/blocks/book-shelf/, and wp-scripts build --blocks-manifest builds it to build/blocks/book-shelf/,
where the existing wp_register_block_types_from_metadata_collection() call registers it with no PHP change. The
lumen/book-card block is the one built in Static and Dynamic Blocks;
this page only adds one context key to it.
lumen-catalog/src/
├── Blocks.php # registers build/blocks/ from blocks-manifest.php
├── BookShelfContext.php # new: applies the shelf's context on the front end
└── blocks/
├── book-card/ # existing static block; gains "usesContext": [ "lumen/showIsbn" ]
└── book-shelf/ # new: block.json, index.js, edit.js, save.js, style.scss
Attributes
Attributes are the block’s data. Each one is declared in the attributes object of block.json with a type (or an
enum), and optionally a source, a selector, a default and a role. The editor holds them in the block’s state;
edit changes them with setAttributes(); save (or render.php) turns them into HTML.
Types, enums and defaults
type |
Notes |
|---|---|
|
JSON scalar types; |
|
JSON structures. With |
|
Since 6.5. The value is a |
"enum": [ "paper", "ink" ] restricts the value to a fixed list, with or without a type. A default is applied when
the attribute is missing; defaults are never written to the post content, so changing a default later changes every
existing block that relied on it.
"role": "content" marks an attribute as user content: content-only editing (templateLock: "contentOnly", synced
pattern overrides) keeps such attributes editable and locks the rest. "role": "local" marks a temporary value that is
never persisted. Until 6.6 the key was __experimentalRole; 6.7 core blocks use role.
Where an attribute is stored: sources
Without a source, an attribute is stored as JSON in the block’s comment delimiter
(<!-- wp:lumen/book-shelf {"showIsbn":false} -->). With a source, it is not stored separately: the editor parses it
out of the saved HTML each time the post is opened. The selector is a CSS selector run inside the block’s own markup
(not inside its inner blocks); without one, the source runs against the block’s root element.
source |
Reads | Typical use |
|---|---|---|
(none) |
the JSON in the comment delimiter |
settings, ids, toggles, anything not visible as text |
|
an HTML attribute ( |
link targets, image |
|
the matched element’s text content, tags stripped |
a plain label |
|
the matched element’s inner HTML |
formatted text (the pre-6.5 RichText pattern) |
|
the matched element’s inner HTML as a |
formatted text with |
|
an array: one object per matched element, each field defined by its own source |
lists (badges, gallery images) |
|
the block’s raw HTML |
the Classic and HTML blocks |
|
a post meta key (deprecated) |
replaced by Block Bindings with |
The meta source is deprecated: binding a block attribute to post meta is done with the Block Bindings API, which
also works on the front end and in templates. See Block Bindings API for
the lp_isbn binding. The children and node sources found in older tutorials are not part of the 7.1 schema.
The shelf’s block.json uses five of these forms, plus the supports discussed below:
{
"$schema": "https://schemas.wp.org/wp/7.1/block.json",
"apiVersion": 3,
"name": "lumen/book-shelf",
"version": "0.1.0",
"title": "Book Shelf",
"category": "lumen-catalog",
"icon": "book",
"description": "A titled shelf of Book Card blocks.",
"keywords": [ "books", "shelf", "catalogue" ],
"textdomain": "lumen-catalog",
"allowedBlocks": [ "lumen/book-card" ],
"attributes": {
"heading": {
"type": "rich-text", "source": "rich-text", "selector": ".wp-block-lumen-book-shelf__heading", "role": "content"
},
"intro": { "type": "string", "source": "html", "selector": ".wp-block-lumen-book-shelf__intro" },
"badges": {
"type": "array",
"source": "query",
"selector": ".wp-block-lumen-book-shelf__badges li",
"query": { "label": { "type": "string", "source": "text" } },
"default": []
},
"moreUrl": {
"type": "string", "source": "attribute", "selector": ".wp-block-lumen-book-shelf__more",
"attribute": "href", "default": "/books/"
},
"moreLabel": {
"type": "string", "source": "text", "selector": ".wp-block-lumen-book-shelf__more", "default": "All books"
},
"showIsbn": { "type": "boolean", "default": true }
},
"providesContext": { "lumen/showIsbn": "showIsbn" },
"supports": {
"html": false,
"anchor": true,
"align": [ "wide", "full" ],
"color": { "background": true, "text": true, "link": true },
"background": { "backgroundImage": true, "backgroundSize": true, "gradient": true },
"spacing": { "padding": true, "margin": [ "top", "bottom" ], "blockGap": true },
"typography": { "fontSize": true, "lineHeight": true, "textIndent": true },
"__experimentalBorder": { "color": true, "radius": true, "style": true, "width": true },
"dimensions": { "minHeight": true },
"shadow": true,
"layout": {
"default": { "type": "grid", "minimumColumnWidth": "14rem" },
"allowSwitching": false
},
"interactivity": { "clientNavigation": true }
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css"
}
The link text is an attribute (moreLabel, text source) rather than a __() call inside save. A translated string
in save is written into the post content, so the same post opened under another locale no longer matches what save
returns and the block is reported as invalid.
What PHP sees
Sources are parsed only by the JavaScript block parser. On the server, parse_blocks() and WP_Block read the
comment delimiter, then apply defaults. A sourced attribute is therefore missing in PHP unless it has a default, and
when it has one, PHP sees the default, not the value in the HTML. Checked on WordPress 7.1.3 with the shelf above,
saved with moreUrl /books/ and moreLabel "All books" (which happen to equal their defaults):
wp eval '$b = new WP_Block( parse_blocks( get_post( 4 )->post_content )[0] ); echo implode( ", ", array_keys( $b->attributes ) );'
Source: Code Reference: WP_Block
showIsbn, backgroundColor, textColor, align, anchor, style, badges, moreUrl, moreLabel
Source: Code Reference: parse_blocks()
heading and intro are absent; badges is [] (its default) even though the saved HTML holds two badges. A dynamic
block, or a render_block filter, that needs a value on the server must keep it in the comment delimiter (no source).
This is the main reason dynamic blocks such as lumen/latest-books use only delimiter attributes.
Editing attributes: RichText, InspectorControls and BlockControls
The edit component of the shelf uses the three standard places to edit attributes: in the canvas (RichText), in
the block toolbar (BlockControls) and in the settings sidebar (InspectorControls).
import { __ } from '@wordpress/i18n';
import {
BlockControls,
InspectorControls,
RichText,
useBlockProps,
useInnerBlocksProps,
} from '@wordpress/block-editor';
import {
PanelBody,
TextControl,
TextareaControl,
ToolbarButton,
ToolbarGroup,
} from '@wordpress/components';
const TEMPLATE = [
[ 'lumen/book-card', { title: 'The Lighthouse Ledger' } ],
[ 'lumen/book-card', { title: 'The Glass Orchard' } ],
];
export default function Edit( { attributes, setAttributes } ) {
const { heading, intro, badges, moreUrl, moreLabel, showIsbn } = attributes;
const blockProps = useBlockProps();
const innerBlocksProps = useInnerBlocksProps(
{ className: 'wp-block-lumen-book-shelf__books' },
{
allowedBlocks: [ 'lumen/book-card' ],
template: TEMPLATE,
templateLock: false,
orientation: 'horizontal',
}
);
return (
<>
<BlockControls group="other">
<ToolbarGroup>
<ToolbarButton
icon="tag"
label={ __( 'Show ISBNs', 'lumen-catalog' ) }
isPressed={ showIsbn }
onClick={ () =>
setAttributes( { showIsbn: ! showIsbn } )
}
/>
</ToolbarGroup>
</BlockControls>
<InspectorControls>
<PanelBody title={ __( 'Shelf', 'lumen-catalog' ) }>
<TextareaControl
__nextHasNoMarginBottom
label={ __( 'Badges (one per line)', 'lumen-catalog' ) }
value={ badges
.map( ( badge ) => badge.label )
.join( '\n' ) }
onChange={ ( value ) =>
setAttributes( {
badges: value
.split( '\n' )
.filter( Boolean )
.map( ( label ) => ( { label } ) ),
} )
}
/>
<TextControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ __( 'All books link', 'lumen-catalog' ) }
value={ moreUrl }
onChange={ ( value ) =>
setAttributes( { moreUrl: value } )
}
/>
<TextControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ __( 'Link text', 'lumen-catalog' ) }
value={ moreLabel }
onChange={ ( value ) =>
setAttributes( { moreLabel: value } )
}
/>
</PanelBody>
</InspectorControls>
<section { ...blockProps }>
<RichText
tagName="h2"
className="wp-block-lumen-book-shelf__heading"
value={ heading }
onChange={ ( value ) =>
setAttributes( { heading: value } )
}
placeholder={ __( 'Shelf title', 'lumen-catalog' ) }
/>
<RichText
tagName="p"
className="wp-block-lumen-book-shelf__intro"
value={ intro }
onChange={ ( value ) => setAttributes( { intro: value } ) }
allowedFormats={ [
'core/bold',
'core/italic',
'core/link',
] }
placeholder={ __( 'Introduction', 'lumen-catalog' ) }
/>
{ badges.length > 0 && (
<ul className="wp-block-lumen-book-shelf__badges">
{ badges.map( ( badge ) => (
<li key={ badge.label }>{ badge.label }</li>
) ) }
</ul>
) }
<div { ...innerBlocksProps } />
<a className="wp-block-lumen-book-shelf__more" href={ moreUrl }>
{ moreLabel }
</a>
</section>
</>
);
}
RichText-
An editable element in the canvas.
tagNameis the element rendered;value/onChangeconnect it to an attribute;allowedFormatslimits the format toolbar (here bold, italic and links;[]allows none);withoutInteractiveFormattingdrops links and similar formats;identifieris needed when a block has several rich text fields and uses the split/merge behaviour. Insave,RichText.Contentwith the sametagNameandclassNamewrites the value; that pair must match the attribute’sselector, or the editor reads nothing back. BlockControls-
A slot in the block toolbar. The
groupprop chooses the position:default,block,inline(next to the formatting buttons),otherandparent. UseToolbarGroup,ToolbarButtonandToolbarDropdownMenufrom@wordpress/componentsinside it. Core alignment toolbars (AlignmentControl,BlockAlignmentControl) are also placed here. InspectorControls-
A slot in the block settings sidebar. Without
groupthe controls go to the Settings tab;group="styles"puts them in the Styles tab,group="advanced"in the Advanced panel, andcolor,typography,dimensions,borderandbackgroundadd items into the panels that the matching supports create. The__next40pxDefaultSizeand__nextHasNoMarginBottomprops opt the components into their current sizing; code without them logs deprecation warnings in the console.
An attribute that a support already provides (alignment, colours, spacing) should never be re-implemented with
InspectorControls: the support also generates the CSS, applies theme presets and respects theme.json settings.
Block supports
A support is a key in supports that tells WordPress to add a feature to the block. For each enabled support
WordPress registers the attributes it needs (align, anchor, backgroundColor, textColor, style, layout, …),
shows the matching controls in the editor and turns the stored values into classes and inline styles. Whether a
control is offered also depends on the theme: most design tools are off unless the theme’s theme.json enables them
(settings.border.radius, settings.spacing.padding, settings.typography.lineHeight, or all at once with
settings.appearanceTools). See theme.json Settings.
Reference for WordPress 7.1
Every key below was checked against the wp-includes/block-supports/ files and the block.json schema of WordPress
7.1.3. "Output" says where the generated classes and styles come from: save means the editor’s save hooks add them to
the static markup; server means WP_Block_Supports adds them when the block is rendered, also for static blocks.
| Key | Sub-keys and values | Output | Since |
|---|---|---|---|
|
|
|
7.0 server |
|
|
|
5.6 |
|
|
none |
|
|
|
|
6.8 |
|
|
inline |
6.4, 6.5, 7.1 |
|
|
|
5.6 |
|
|
|
5.8 |
|
|
inline styles |
6.2, 6.5, 7.0, 7.1 |
|
|
none |
|
|
|
the server processes |
6.5 |
|
|
|
5.8; grid 6.3 |
|
|
inline |
6.3 |
|
|
padding and margin inline; |
5.8 |
|
|
classes for preset sizes, inline styles otherwise |
5.6; |
|
|
wrapper classes |
5.6 |
|
|
none |
various; |
A few details that the reference pages leave implicit:
-
Border is still declared as
__experimentalBorderin 7.1.3, both in core blocks'block.jsonand in the PHP and JavaScript support code; aborderkey is ignored. The stored values live instyle.borderregardless. -
Background gradient (
background.gradient, 7.1) is separate fromcolor.gradients. When both are supported and a background gradient is set, 7.1 suppresses the colour gradient output. The background support is rendered on the server only, so a static block’s saved markup does not contain it (see the outputs below). Core’s owntheme.jsonenablessettings.background.gradientby default. -
Text indent (
typography.textIndent, 7.0) writestext-indenton the wrapper. The theme settingsettings.typography.textIndenttakes"subsequent"(the default: indent paragraphs that follow a paragraph),"all"orfalse; it concerns the Paragraph block’s global style, not the per-block value. -
To keep the controls but write the output yourself (for instance on an inner element in
render.php), set__experimentalSkipSerialization(trueor a list of features) inside the support object; WordPress then stores the values but does not add classes or styles. -
The Block Editor Handbook follows the Gutenberg development branch. It lists
typography.textShadowandbackground.backgroundClip("since WordPress 7.2"), which are not available in 7.1.3; use them only once 7.2 ships.
Source: Block Editor Handbook: Supports
Serialized markup
The block was created with every support set, then serialized and parsed back by the block editor’s own packages
running in Node (@wordpress/blocks with the core and Lumen blocks registered). The shelf and its two cards were
reported valid. Supports values are stored in the delimiter; the sourced attributes are only in the HTML:
<!-- wp:lumen/book-shelf {"showIsbn":false,"backgroundColor":"paper","textColor":"ink","align":"wide","anchor":"autumn","style":{"spacing":{"padding":{"top":"2rem","bottom":"2rem"},"blockGap":"1.5rem"},"typography":{"textIndent":"1em"},"border":{"radius":"8px","width":"1px","style":"solid","color":"#1b1b1b"},"background":{"gradient":"linear-gradient(135deg,#fdf6e3 0%,#f1e3c3 100%)"},"dimensions":{"minHeight":"20rem"}}} -->
<section class="wp-block-lumen-book-shelf alignwide has-border-color has-ink-color has-paper-background-color has-text-color has-background" id="autumn" style="border-color:#1b1b1b;border-style:solid;border-width:1px;border-radius:8px;min-height:20rem;padding-top:2rem;padding-bottom:2rem;text-indent:1em"><h2 class="wp-block-lumen-book-shelf__heading">Autumn shelf</h2><p class="wp-block-lumen-book-shelf__intro">New <em>and</em> forthcoming titles.</p><ul class="wp-block-lumen-book-shelf__badges"><li>Signed copies</li><li>New this month</li></ul><div class="wp-block-lumen-book-shelf__books"><!-- wp:lumen/book-card {"isbn":"978-1-23456-789-7"} -->
<article class="wp-block-lumen-book-card"><h3 class="wp-block-lumen-book-card__title">The Lighthouse Ledger</h3><p class="wp-block-lumen-book-card__isbn">978-1-23456-789-7</p><!-- wp:paragraph -->
<p>Short blurb.</p>
<!-- /wp:paragraph --></article>
<!-- /wp:lumen/book-card -->
<!-- wp:lumen/book-card {"isbn":"978-1-23456-790-3"} -->
<article class="wp-block-lumen-book-card"><h3 class="wp-block-lumen-book-card__title">The Glass Orchard</h3><p class="wp-block-lumen-book-card__isbn">978-1-23456-790-3</p><!-- wp:paragraph -->
<p>Short blurb.</p>
<!-- /wp:paragraph --></article>
<!-- /wp:lumen/book-card --></div><a class="wp-block-lumen-book-shelf__more" href="/books/">All books</a></section>
<!-- /wp:lumen/book-shelf -->
The gradient and the blockGap are in the delimiter but not in the HTML: those supports are applied on the server.
Rendered output
The same post content, rendered by WordPress 7.1.3 with apply_filters( 'the_content', ... ) and the block-supports
stylesheet printed afterwards. The gradient is now an inline background-image, the layout classes are on the
inner-blocks wrapper rather than on <section>, and the ISBNs are gone because the shelf’s context turned them off
(see Block context):
<section class="wp-block-lumen-book-shelf alignwide has-border-color has-ink-color has-paper-background-color has-text-color has-background" id="autumn" style="border-color:#1b1b1b;border-style:solid;border-width:1px;border-radius:8px;min-height:20rem;padding-top:2rem;padding-bottom:2rem;text-indent:1em;background-image:linear-gradient(135deg,#fdf6e3 0%,#f1e3c3 100%);"><h2 class="wp-block-lumen-book-shelf__heading">Autumn shelf</h2><p class="wp-block-lumen-book-shelf__intro">New <em>and</em> forthcoming titles.</p><ul class="wp-block-lumen-book-shelf__badges"><li>Signed copies</li><li>New this month</li></ul><div class="wp-block-lumen-book-shelf__books is-layout-grid wp-container-lumen-book-shelf-is-layout-5dcdb940 wp-block-lumen-book-shelf-is-layout-grid">
<article class="wp-block-lumen-book-card"><h3 class="wp-block-lumen-book-card__title">The Lighthouse Ledger</h3>
<p class="wp-block-paragraph">Short blurb.</p>
</article>
<article class="wp-block-lumen-book-card"><h3 class="wp-block-lumen-book-card__title">The Glass Orchard</h3>
<p class="wp-block-paragraph">Short blurb.</p>
</article>
</div><a class="wp-block-lumen-book-shelf__more" href="/books/">All books</a></section>
.wp-container-lumen-book-shelf-is-layout-5dcdb940 {
grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
container-type: inline-size;
gap: 1.5rem;
}
The layout support finds the inner-blocks wrapper by looking for the last unclosed element with a class attribute
before the first inner block in the saved markup; giving that wrapper a class (wp-block-lumen-book-shelf__books) is
what makes the grid apply to the cards and not to the heading. A block whose wrapper is also the inner-blocks container
needs nothing special.
InnerBlocks
A block that contains other blocks renders an inner-blocks area in edit and writes it back in save. The
useInnerBlocksProps() hook (preferred over the older <InnerBlocks /> component) returns props to spread on
the element that will hold the children, so the wrapper can carry its own class, and so the parent can avoid an extra
<div>: useInnerBlocksProps( useBlockProps(), options ) makes the block wrapper itself the container.
import {
RichText,
useBlockProps,
useInnerBlocksProps,
} from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { heading, intro, badges, moreUrl, moreLabel } = attributes;
// Call useBlockProps.save() first: useInnerBlocksProps.save() serializes
// the inner blocks, which resets the props provider used by useBlockProps.
const blockProps = useBlockProps.save();
const innerBlocksProps = useInnerBlocksProps.save( {
className: 'wp-block-lumen-book-shelf__books',
} );
return (
<section { ...blockProps }>
<RichText.Content
tagName="h2"
className="wp-block-lumen-book-shelf__heading"
value={ heading }
/>
<RichText.Content
tagName="p"
className="wp-block-lumen-book-shelf__intro"
value={ intro }
/>
{ badges.length > 0 && (
<ul className="wp-block-lumen-book-shelf__badges">
{ badges.map( ( badge ) => (
<li key={ badge.label }>{ badge.label }</li>
) ) }
</ul>
) }
<div { ...innerBlocksProps } />
<a className="wp-block-lumen-book-shelf__more" href={ moreUrl }>
{ moreLabel }
</a>
</section>
);
}
The order of the two save hooks matters. useInnerBlocksProps.save() serializes the children immediately, and each
child’s serialization overwrites the module-level state that useBlockProps.save() reads. Called after it,
useBlockProps.save() returns the last child’s props: the shelf was saved as a bare <section> with no class,
alignment, colours or anchor, and the editor reported the block as invalid the next time the post was opened. Calling
useBlockProps.save() first, as above, or passing its result into useInnerBlocksProps.save( useBlockProps.save() ),
avoids it.
Allowed blocks, parent and ancestor
-
allowedBlocksinblock.json(6.5) lists the block types that may be direct children; theallowedBlocksoption ofuseInnerBlocksPropsnarrows it further at run time (for example from an attribute). The shelf allows onlylumen/book-card, so the inserter inside it offers nothing else. -
parentin a child’sblock.jsonlimits where the child may be inserted to direct children of the listed blocks;ancestor(6.0) allows any depth below them.lumen/book-carddeclares neither, because cards are also placed on their own in posts and in the Query Loop; adding"parent": [ "lumen/book-shelf" ]would hide it from the inserter everywhere else.
Template and templateLock
template is the list of blocks inserted when the inner area is empty, each entry [ name, attributes, innerBlocks ].
The shelf starts with two cards titled after two catalogue books. templateLock controls what the editor may change:
templateLock |
Effect |
|---|---|
|
No locking, and the block does not inherit a lock from its parent. The shelf uses this so editors can add, move and remove cards. |
(unset) |
Inherits the lock of the nearest locked parent (or of the post type template). |
|
Existing blocks can be moved, but none can be added or removed. |
|
Nothing can be added, removed or moved. |
|
Only attributes with |
Other options of useInnerBlocksProps: orientation: 'horizontal' turns the movers into left/right arrows (it does not
change layout); defaultBlock and directInsert make the appender insert a given block at once; renderAppender
replaces the appender (InnerBlocks.ButtonBlockAppender, InnerBlocks.DefaultBlockAppender, or false);
templateInsertUpdatesSelection controls whether inserting the template moves focus into it. A block’s own lock
("lock": { "move": true, "remove": true }, the lock attribute every block gets) is separate from templateLock,
and the lock support set to false removes the lock control.
Source: Block Editor Handbook: Templates
Templates for a whole post type ('template' and 'template_lock' in register_post_type()) use the same format and
lock values; see Custom Post Types and Taxonomies.
Block context
Block context lets an ancestor hand values to descendants without the descendants copying them into their own
attributes. The provider maps context names to its own attributes with providesContext; a consumer lists the names it
wants in usesContext. Names are namespaced like block names (lumen/showIsbn). Core provides postId and
postType (inside the Query Loop’s Post Template these are the current post’s), queryId, query and others; a block
receives a value only when its usesContext names it, as lumen/latest-books does with postId and postType.
The shelf provides lumen/showIsbn from its showIsbn attribute (see the block.json above). The card’s block.json
of block.json and Registration has only providesContext, so it
gains the field: "usesContext": [ "lumen/showIsbn" ]. In the editor,
context arrives as the context prop of edit. The card’s edit reads it and renders the ISBN paragraph only when
the shelf has not turned it off (undefined, outside a shelf, counts as "show"):
export default function Edit( { attributes, setAttributes, context } ) {
const { title, isbn, coverUrl, showIsbn } = attributes;
const shelfShowsIsbn = context[ 'lumen/showIsbn' ] !== false;
// The rest of the component is unchanged except the ISBN condition, which becomes
// { shelfShowsIsbn && showIsbn && isbn && ( <p className="wp-block-lumen-book-card__isbn">{ isbn }</p> ) }
}
Context is not available to save. A static block’s saved markup cannot depend on its parent, because the same
markup must validate wherever the block is moved. The card therefore keeps saving its ISBN, and the decision is taken
when the page is rendered. On the server, WP_Block::$context holds the values a block declared in usesContext
(and only those): a dynamic block reads $block->context['lumen/showIsbn'] in render.php, and a static one is
adjusted with the render_block_{$name} filter, whose third argument is the WP_Block instance:
<?php
/**
* Applies the lumen/showIsbn context to saved Book Card markup.
*
* @package Lumen\Catalog
*/
namespace Lumen\Catalog;
defined( 'ABSPATH' ) || exit;
/**
* Hides the ISBN of every Book Card placed in a shelf that turned ISBNs off.
*/
final class BookShelfContext {
/**
* Hooks the filter. Called from Plugin::boot().
*/
public static function boot(): void {
add_filter( 'render_block_lumen/book-card', array( self::class, 'filter_card' ), 10, 3 );
}
/**
* Removes the ISBN paragraph when the parent shelf provides showIsbn = false.
*
* @param string $content Saved block HTML.
* @param array $block Parsed block.
* @param \WP_Block $instance Block instance; context holds only "usesContext" keys.
* @return string Block HTML.
*/
public static function filter_card( string $content, array $block, \WP_Block $instance ): string {
if ( false !== ( $instance->context['lumen/showIsbn'] ?? true ) ) {
return $content;
}
return (string) preg_replace(
'#<p class="wp-block-lumen-book-card__isbn">.*?</p>#s',
'',
$content
);
}
}
With showIsbn false on the shelf the rendered page contains no __isbn paragraph (the output above); removing
"showIsbn":false from the delimiter brings both back. The filter only removes markup the block itself saved; for
structured changes to the HTML, WP_HTML_Tag_Processor is safer than a regular expression.
Context values pass through any depth of nesting, are recomputed when the provider’s attribute changes in the editor,
and are not stored in the child’s markup, so they never invalidate it. A consumer outside any provider receives nothing
for that key. Context is also how Block Bindings sources and the Query Loop give blocks the current post:
Block Bindings API builds on the same usesContext mechanism.
Verifying a block
After npm run build (wp-scripts build --blocks-manifest, see
Block Development Environment) and wp-scripts lint-js, the
registry on a WordPress 7.1.3 site shows the attributes that the supports added on top of the declared ones:
wp eval '$t = WP_Block_Type_Registry::get_instance()->get_registered( "lumen/book-shelf" ); echo implode( ",", array_keys( $t->attributes ) );'
Source: WP-CLI Commands: wp eval
heading,intro,badges,moreUrl,moreLabel,showIsbn,lock,metadata,align,className,style,backgroundColor,textColor,fontSize,borderColor,layout,anchor
style holds every free-form value (spacing, border, typography, background, dimensions, shadow); the separate
backgroundColor, textColor, fontSize and borderColor attributes hold preset slugs. Both block.json files also
validate against the 7.1 JSON schema, and the parse-and-compare check used above (parse() from @wordpress/blocks
and the isValid flag of each parsed block) can run in a Jest test.
Pitfalls and older material
| Pitfall or outdated advice | What applies in WordPress 7.1 |
|---|---|
Calling |
The wrapper loses all its props and the block becomes invalid; call |
Expecting sourced attributes in |
PHP sees only delimiter attributes plus defaults; keep server-needed values unsourced. |
Using |
Output must be deterministic; translated or time-dependent text belongs in an attribute or in a dynamic block. |
Changing a |
Existing posts become invalid; add a |
|
Deprecated; use Block Bindings with |
|
Renamed |
|
Not recognised in 7.1.3; use |
|
Use |
The older books in the bibliography do not cover block development, so none of this material appears in them: they
predate block.json attributes as the standard, the rich-text type, server-rendered supports, the grid layout and
every support added from 6.4 onward. One of them still speaks of "reusable blocks", which are synced patterns since
6.3; synced pattern overrides, which rely on "role": "content", are covered in
Block Bindings API.