WP Manifestindependent plugin directory
manifest / developer / responsive-block-controls

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

★ 0stars
0forks

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

Responsive 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

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:

  1. Both Gutenberg and this plugin are already active.
  2. Insert Feature Grid and add a few items.
  3. Open the device preview dropdown and enable Responsive editing.
  4. Switch to Tablet or Mobile (or drag the canvas resize handles) — the viewport badge appears in the inspector.
  5. Change Columns — the control shows it's editing that viewport, and the grid updates only at that canvas width.
  6. Also try the block-supports side by comparison: change the background color on Tablet — that one is handled by core automatically.
  7. 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.