WordPress 7.1 added responsive styling to the block editor. Basically, you can now define how a block looks at different screen sizes without writing a line of CSS. It’s one of the longest-standing requests in block-theme land, and it landed alongside a few related pieces: configurable breakpoints, pseudo-state styling, and – importantly – some new controls for managing it’s use and access.
This post is more technical than “hey, it shipped” – we hit the theme.json syntax, the defaults, the constraints, and how it fits with the fluid typography and spacing tokens you may already have in your theme.
Responsive Styling
Responsive styling is available in two places: Global Styles (these are scoped more globally) and individual block instances (more locally scoped – on a block-by-block basis).
There are two default “viewports” set:
@mobile @media (width <= 480px)
@tablet @media (480px < width <= 782px)
Notice there’s no @desktop style – that’s because that’s the default base style – any @ reference will override that default.
Theme.JSON Syntax
Block-level (Global) responsive styles nest a viewport key inside the block’s style object:
"styles": {
"blocks": {
"core/group": {
"spacing": {
"padding": { "top": "3rem", "right": "3rem", "bottom": "3rem", "left": "3rem" }
},
"@mobile": {
"spacing": {
"padding": { "top": "1rem", "right": "1rem", "bottom": "1rem", "left": "1rem" }
}
}
}
}
}
That’s 3rem of padding everywhere, dropping to 1rem at the 480px breapoint and below. Notice that the structure in theme.json is the same, just scoped more specifically to the @mobile object. Every instance of that block is affected at all levels in a site.
Instance-level styling is found in the block’s style attribute as you look at the post content:
<!-- wp:paragraph {"style":{"@mobile":{"typography":{"fontSize":"1rem"}}}} -->
<p>Text with a responsive font size.</p>
<!-- /wp:paragraph -->
Two notes, because i know someone’s going to ask about it:
- Non-layout per-instance declarations are output with
!importantso they can override inline styles. Yes, it drives me nuts, too. - Layout values and
blockGapgo through the existing layout support rather than that path. Which, is better anyway since we’re separating content with layout anyway – or we should be, at least.
Setting Your Own Breakpoints
A settings.viewport object in the theme.json can let you override the defaults with your own:
"settings": {
"viewport": { "mobile": "30rem", "tablet": "45rem" }
}
This will generate, in CSS:
@media (width <= 30rem) { /* Mobile */ }
@media (30rem < width <= 45rem) { /* Tablet */ }
One thing to note: you can only use px, em, or rem values here – no %s. The default (782px) matches the breakpoint WordPress already uses natively in its admin screens. If you already have breakpoints set in CSS (using @media queries, most likely) you should match those up to your theme.json objects using the above code: otherwise, you’ll end up with conflicting breakpoints.
pseudo- and custom style states
This is a long-standing feature that I’ve been asking for a long time. While it’s not exactly the same as the responsive styling, it uses the same underlying style-states, so I want to mention it here.
Currently, the supported pseudo states are limited to the usual suspects: :hover, :focus, :focus-visible, and :active. Which, if you’re familiar with these uses, means the next limitation is no surprise – these are only really supported officially on Button and Navigation Link blocks. If you use these states anywhere else, you’ll want to keep using your own CSS for now.
That said, the same syntax to responsive breakpoints applies here as well:
"styles": {
"blocks": {
"core/button": {
"color": { "background": "black", "text": "white" },
":hover": {
"color": { "background": "blue" }
}
}
}
}
…and can live inside of viewports, which brings us full circle:
"@mobile": {
":hover": {
"color": { "background": "var:preset|color|contrast", "text": "var:preset|color|base" }
}
}
It can also live in the post content, but I’d not recommend that – doing it this way renders it invisible to your design system. Best to keep it tokenized in theme.json. BUT, if you need the override, you can use it:
<!-- wp:button {"backgroundColor":"accent-3","style":{":hover":{"color":{"background":"var:preset|color|accent-2"}}}} -->
One note: Navigation links also support the :current state – which showcases which link you are actively navigated to. This state, plus other custom states, generate a CSS class, and require registration through the block.json‘s selectors property:
":current": {
"color": { "text": "var:preset|color|contrast" }
}
How can I disable the UI?
Two filters/settings exist, and they behave the same way:
function example_disable_responsive_editing( $settings ) {
$settings['responsiveEditingEnabled'] = false;
return $settings;
}
add_filter( 'block_editor_settings_all', 'example_disable_responsive_editing' );
responsiveEditingEnabledcontrols the responsive editing interface.blockStatesEditingEnableddoes the same for pseudo-state editing. Defaults totrue.
That said, both govern the editing interface only. Any styles saved into theme.json, in Global Styles, or in the style attribute on a block are left alone with this filter. Media query CSS is still also generated. This only removes the UI, not the functionality.
I’m already using token and primitives… how does this fit in?
The main thought here is that the viewport styles change at a threshold, while the fluid tokens can change continuously. Both are responsive, but solve different problems. Something like clamp()responds at 900px – which no default viewport covers. Viewport styles can also control layout changes, like changing a row to a column for organizational purposes.
Presets Become CSS Custom Properties
Everything defined in theme.json presets is emitted as a custom property:
--wp--preset--color--primary--wp--preset--font-size--large--wp--custom--*
And so on. This works for anything under the settings.custom object. Those tokens are available to you in your stylesheet, as well – without needing to refine them.
Fluid Typography
Setting "fluid": true in the typography settings, with per-size min and max values, produces font sizes that work between viewport widths. This is the mechanism that handles continuous type scaling; no media query is involved.
Clamp() in the spacing scale
Spacing presets accept clamp(), which does for rhythm what fluid typography does for type – a section gap that compresses smoothly as the viewport narrows.
Available Constraints
These may seem familiar to anyone who’s in theme.json regularly, but these options are also availble to you:
settings.color.custom— the custom color pickersettings.typography.customFontSize— arbitrary font-size entrysettings.spacing.customSpacingSize— arbitrary spacing entry
Set to false, these allow choice from presets rather than arbitrary values. This works well with the “Decisions, Not Options” model that has governed WordPress in the past – and makes for much easier client editing, in my opinion.
Naming Conventions
Preset slugs become the names editors see in the interface and the custom-property names in your CSS. Slugs that describe a role (primary, base, border-light) stay accurate through a palette change; slugs that describe the value (blue-500) don’t.
So, what’s missing?
This is pretty all-encompassing, and we’re recapping a few, but there’s some notable things either missing or coming soon:
- Pseudo states are limited to Button and Navigation Link.
- This is limited to two viewports, not an arbitrary number.
- Breakpoint values are limited to plain lengths. That means no CSS functions or percentages.
- Container queries are not part of this; these are viewport-width media queries.
Quick Reference and Sources
| Thing | Syntax | Notes |
|---|---|---|
| Mobile styles | "@mobile": { ... } | ≤480px by default |
| Tablet styles | "@tablet": { ... } | 480–782px by default |
| Desktop styles | (the base style object) | No @desktop key exists |
| Custom breakpoints | settings.viewport.mobile / .tablet | px, em, rem only |
| Pseudo states | ":hover", ":focus", ":focus-visible", ":active" | Button + Nav Link only |
| Nav Link current item | ":current" | Custom state, class-based |
| Nesting | "@mobile": { ":hover": { ... } } | Viewport wraps state |
| Disable responsive UI | responsiveEditingEnabled | UI only; saved styles still render |
| Disable states UI | blockStatesEditingEnabled | Defaults to true |


