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

string, boolean, integer, number, null

JSON scalar types; number accepts floats as well as integers.

array, object

JSON structures. With "source": "query" the shape of each item comes from the query definition.

rich-text

Since 6.5. The value is a RichTextData object in the editor and serializes to HTML. Core text blocks (the Paragraph content attribute, for example) moved from "type": "string", "source": "html" to "type": "rich-text", "source": "rich-text" in 6.5. Use String( value ) when plain HTML text is needed in JavaScript.

"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

attribute

an HTML attribute ("attribute": "href") of the matched element

link targets, image src/alt

text

the matched element’s text content, tags stripped

a plain label

html

the matched element’s inner HTML

formatted text (the pre-6.5 RichText pattern)

rich-text

the matched element’s inner HTML as a RichTextData value

formatted text with RichText (6.5+)

query

an array: one object per matched element, each field defined by its own source

lists (badges, gallery images)

raw

the block’s raw HTML

the Classic and HTML blocks

meta

a post meta key (deprecated)

replaced by Block Bindings with core/post-meta

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 ) );'
showIsbn, backgroundColor, textColor, align, anchor, style, badges, moreUrl, moreLabel

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. tagName is the element rendered; value/onChange connect it to an attribute; allowedFormats limits the format toolbar (here bold, italic and links; [] allows none); withoutInteractiveFormatting drops links and similar formats; identifier is needed when a block has several rich text fields and uses the split/merge behaviour. In save, RichText.Content with the same tagName and className writes the value; that pair must match the attribute’s selector, or the editor reads nothing back.

BlockControls

A slot in the block toolbar. The group prop chooses the position: default, block, inline (next to the formatting buttons), other and parent. Use ToolbarGroup, ToolbarButton and ToolbarDropdownMenu from @wordpress/components inside it. Core alignment toolbars (AlignmentControl, BlockAlignmentControl) are also placed here.

InspectorControls

A slot in the block settings sidebar. Without group the controls go to the Settings tab; group="styles" puts them in the Styles tab, group="advanced" in the Advanced panel, and color, typography, dimensions, border and background add items into the panels that the matching supports create. The __next40pxDefaultSize and __nextHasNoMarginBottom props 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

anchor

true: adds the anchor attribute and an HTML anchor field

id on the wrapper (save; server-side support added in 7.0 for dynamic blocks)

7.0 server

align

true or a list of left, center, right, wide, full

alignwide and similar classes (save and server)

5.6

alignWide

false hides wide/full for this block

none

ariaLabel

true: an ariaLabel attribute

aria-label on the wrapper

6.8

background

backgroundImage, backgroundSize, gradient

inline background-image and related properties (server only)

6.4, 6.5, 7.1

color

background, text, link, gradients, heading, button, enableContrastChecker

has-<slug>-color, has-background, inline colours (save and server); link and element colours as generated CSS

5.6

__experimentalBorder

color, radius, style, width

has-border-color, inline border-* (save and server)

5.8

dimensions

minHeight, aspectRatio, width, height, minWidth

inline styles

6.2, 6.5, 7.0, 7.1

html

false removes Edit as HTML

none

interactivity

true, or interactive and clientNavigation

the server processes data-wp-* directives; clientNavigation marks the block safe for client-side navigation

6.5

layout

default (flow, constrained, flex, grid and their options), allowSwitching, allowEditing, allowInheriting, allowSizingOnChildren, allowJustification, allowOrientation, allowVerticalAlignment

is-layout-* classes and a generated .wp-container-* rule (server only)

5.8; grid 6.3

shadow

true

inline box-shadow from a preset

6.3

spacing

padding, margin (true or a list of sides), blockGap

padding and margin inline; blockGap through the layout rule (server only)

5.8

typography

fontSize, lineHeight, textAlign, textColumns, textIndent, and the still experimental __experimentalFontFamily, __experimentalFontWeight, __experimentalFontStyle, __experimentalLetterSpacing, __experimentalTextTransform, __experimentalTextDecoration, __experimentalWritingMode

classes for preset sizes, inline styles otherwise

5.6; textIndent 7.0

className, customClassName

false removes the generated wp-block-* class or the Additional CSS class field

wrapper classes

5.6

lock, multiple, inserter, reusable, renaming, visibility

false disables locking, more than one instance, the inserter, saving as a pattern, renaming in List View, the visibility controls

none

various; visibility 6.9

A few details that the reference pages leave implicit:

  • Border is still declared as __experimentalBorder in 7.1.3, both in core blocks' block.json and in the PHP and JavaScript support code; a border key is ignored. The stored values live in style.border regardless.

  • Background gradient (background.gradient, 7.1) is separate from color.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 own theme.json enables settings.background.gradient by default.

  • Text indent (typography.textIndent, 7.0) writes text-indent on the wrapper. The theme setting settings.typography.textIndent takes "subsequent" (the default: indent paragraphs that follow a paragraph), "all" or false; 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 (true or 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.textShadow and background.backgroundClip ("since WordPress 7.2"), which are not available in 7.1.3; use them only once 7.2 ships.

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

  • allowedBlocks in block.json (6.5) lists the block types that may be direct children; the allowedBlocks option of useInnerBlocksProps narrows it further at run time (for example from an attribute). The shelf allows only lumen/book-card, so the inserter inside it offers nothing else.

  • parent in a child’s block.json limits where the child may be inserted to direct children of the listed blocks; ancestor (6.0) allows any depth below them. lumen/book-card declares 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

false

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).

"insert"

Existing blocks can be moved, but none can be added or removed.

"all"

Nothing can be added, removed or moved.

"contentOnly"

Only attributes with "role": "content" stay editable; other blocks are hidden from List View and cannot be selected. Children cannot override it.

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.

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 ) );'
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 useInnerBlocksProps.save() before useBlockProps.save()

The wrapper loses all its props and the block becomes invalid; call useBlockProps.save() first.

Expecting sourced attributes in render.php or a render_block filter

PHP sees only delimiter attributes plus defaults; keep server-needed values unsourced.

Using __() or a date in save

Output must be deterministic; translated or time-dependent text belongs in an attribute or in a dynamic block.

Changing a selector, source or the markup save returns

Existing posts become invalid; add a deprecated entry (see Variations, Styles, Transforms and Deprecations).

"source": "meta" to edit post meta

Deprecated; use Block Bindings with core/post-meta, or useEntityProp in the editor.

__experimentalRole

Renamed role (6.7).

border as a support key

Not recognised in 7.1.3; use __experimentalBorder.

templateLock: "all" to protect content

Use "contentOnly" when editors should still edit text and images.

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.