Feature Grid (Responsive Controls Demo)
Demo: a custom block control (column count) opted into WordPress 7.1 responsive styling style states.
by Ryan Welcher · github.com/ryanwelcher/responsive-block-controls
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/ryanwelcher/responsive-block-controls/archive/refs/heads/trunk.zipResponsive Block Controls Demo
A demo WordPress plugin showing how to opt a custom block control into the responsive styling ("style states") system introduced for WordPress 7.1.
It registers a single block — Feature Grid — a grid container whose custom Columns control can hold a different value per viewport (Desktop / Tablet / Mobile), edited through the same Responsive editing flow core blocks use, and rendered on the frontend with real media queries.
⚠️ Experimental. There is no public API for third-party blocks to integrate with style states yet — block supports for states were deliberately removed (gutenberg#78088) until the API is finalized. This demo unlocks a private editor selector and degrades gracefully when it's unavailable. Learn from it; don't ship it.
Background
- Call for testing: Responsive styling
- Tracking issue: Responsive style states for blocks (WP 7.1)
- PR: Responsive block instance styles
- PR: Integrate Resizable Editor with Device Preview and add Responsive editing
Blocks built on standard block supports (color, typography, spacing, border, dimensions, layout) get responsive styling for free. Custom controls do not — per the call for testing: "if a block implements custom controls instead of block supports, responsive styles won't apply to those." This repo shows what it takes to bridge that gap today.
How the system works
1. Storage: viewport keys in the style attribute
Core stores per-viewport overrides inside the block's style attribute, keyed by viewport. Desktop is the base value; @tablet and @mobile hold overrides. This block follows the same convention for its custom value:
{
"columnCount": 3,
"style": {
"@tablet": { "columnCount": 2 },
"@mobile": { "columnCount": 1 }
}
}
2. Editor: which viewport is being edited?
The editor tracks a globally selected style state — switching the device preview or resizing the canvas (with Responsive editing enabled) sets its viewport to default, @tablet, or @mobile. It's exposed via the private getSelectedBlockStyleState( clientId ) selector on the core/block-editor store.
src/feature-grid/style-state.js unlocks it the same way core packages share private APIs:
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules(
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
'@wordpress/block-library'
);
const { getSelectedBlockStyleState } = unlock( select( 'core/block-editor' ) );
const { viewport } = getSelectedBlockStyleState( clientId );
The consent string means what it says. The demo wraps this in try/catch and falls back to Desktop-only editing if the private API moves.
The Columns control then simply reads/writes the value for the selected viewport (see setValueForViewport() in style-state.js), labels itself with the viewport being edited, and resets per viewport via the ToolsPanel menu.
3. The inspector filters controls in state mode
This one will bite you: when a viewport (or pseudo) state is selected, the block inspector stops rendering the default InspectorControls group entirely — it only renders the style group slots: typography, color, background, layout (viewport states only), dimensions, and border. A control in a plain PanelBody simply disappears the moment Responsive editing kicks in.
The demo therefore registers its Columns control as a ToolsPanelItem inside InspectorControls group="layout":
<InspectorControls group="layout">
<ToolsPanelItem panelId={ clientId } label="Columns" … >
<RangeControl … />
</ToolsPanelItem>
</InspectorControls>
That keeps it visible on Desktop and while editing a viewport — and core hides the layout group for pseudo states like hover, which is exactly what you want for a structural control.
Two placement quirks that follow from this, both matching core behavior:
- On Desktop, the control lives in the Styles tab of the block inspector (style group slots render there; the Settings tab only renders the default group). While editing a viewport there are no tabs, so it shows directly.
- The control never shows for pseudo states (hover, etc.), since core only renders the layout group for viewport-only states.
4. Breakpoints: theme.json settings.viewport
Media queries are derived from theme.json's settings.viewport breakpoints, defaulting to:
{ "settings": { "viewport": { "mobile": "480px", "tablet": "782px" } } }
Which produces (matching WP_Theme_JSON::get_viewport_media_queries()):
@mobile→@media (width <= 480px)@tablet→@media (480px < width <= 782px)- Desktop → base rule, no media query (desktop-first cascade)
Note the queries don't overlap: a mobile viewport without an override inherits the Desktop value, not the tablet one.
5. Frontend: generate your own CSS
Core's state renderer (gutenberg_render_block_states_support()) only emits CSS for block supports. A custom control has to render its own media queries — src/feature-grid/render.php builds a per-instance rule set setting a --feature-grid-columns custom property, using core's breakpoint helper when available.
6. Editor preview for free
Because the post editor canvas is an iframe, the block outputs the same media-query CSS in edit.js — resizing the canvas or switching device previews changes the iframe width and the right rule applies. No JS viewport simulation needed.
Trying it
Requires the Gutenberg plugin (the responsive editing UI is still landing; use the latest release or a nightly during the WP 7.1 cycle).
npm install
npm run build
Then start wp-env (requires Docker; the bundled .wp-env.json installs the latest stable Gutenberg alongside this plugin):
npm run wp-env start
Log in at http://localhost:8888/wp-admin (admin / password), then:
- Both Gutenberg and this plugin are already active.
- Insert Feature Grid and add a few items.
- Open the device preview dropdown and enable Responsive editing.
- Switch to Tablet or Mobile (or drag the canvas resize handles) — the viewport badge appears in the inspector.
- Change Columns — the control shows it's editing that viewport, and the grid updates only at that canvas width.
- Also try the block-supports side by comparison: change the background color on Tablet — that one is handled by core automatically.
- Save and check the frontend at different window widths.
WordPress Playground
With the build directory committed, the included blueprint runs the demo in your browser:
https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/ryanwelcher/responsive-block-controls/trunk/_playground/blueprint.json
File tour
| File | What to look at |
|---|---|
src/feature-grid/style-state.js |
All the style-state plumbing: viewport keys, private-API unlock, cascade-aware getters/setters, media-query builder. |
src/feature-grid/edit.js |
A viewport-aware ToolsPanelItem/RangeControl in the layout group, and per-instance editor CSS. |
src/feature-grid/render.php |
Frontend media-query generation from theme.json breakpoints. |
src/feature-grid/block.json |
Standard supports (color/spacing/typography) — responsive for free, for contrast with the custom control. |