Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2913b8c
Allow setting viewport tablet and mobile values in theme.json
tellthemachines Jun 11, 2026
1f20bca
restrict allowed units
tellthemachines Jun 11, 2026
f9f7e29
adjust device preview to use theme.json viewports where available
tellthemachines Jun 12, 2026
9eec829
Accept and process em/rem values for block preview
tellthemachines Jun 18, 2026
b027cc5
remove old constant and rename new one
tellthemachines Jun 18, 2026
aa3290e
remove order check
tellthemachines Jun 18, 2026
40895ab
dedupe sanitization
tellthemachines Jun 18, 2026
9e30e7e
allow $responsive_media_queries to be null
tellthemachines Jun 19, 2026
8cb7b52
use get_responsive_media_queries instead of get_viewport_breakpoints
tellthemachines Jun 22, 2026
50d9cf6
fix grid visualiser visibility with custom breakpoints
tellthemachines Jun 22, 2026
46cd3ed
fix conflict resolution booboo
tellthemachines Jun 23, 2026
02fdda7
ensure device preview works with custom breakpoints
tellthemachines Jun 26, 2026
bd9abee
Process invalid or absent viewport values consistently.
tellthemachines Jun 29, 2026
c5b589c
document em and rem to px conversion
tellthemachines Jul 2, 2026
30eb14d
clarify invalid breakpoint handling
tellthemachines Jul 2, 2026
51aaa2b
change function name
tellthemachines Jul 2, 2026
532a447
rearrange sanitization logic
tellthemachines Jul 2, 2026
8a7abd2
dedupe value check
tellthemachines Jul 2, 2026
483ceb5
add docs to theme json functions
tellthemachines Jul 2, 2026
23143ef
add some more tests
tellthemachines Jul 2, 2026
82515c7
remove test file deleted on trunk
tellthemachines Jul 3, 2026
face46c
update viewport_settings type
tellthemachines Jul 3, 2026
b664302
simplify sanitization and docs update
tellthemachines Jul 3, 2026
b31e166
don't export const
tellthemachines Jul 3, 2026
72cbf48
make viewport APIs private
tellthemachines Jul 3, 2026
f2dc7f2
avoid unnecessary rerenders in states hook
tellthemachines Jul 3, 2026
5571cc7
remove effect
tellthemachines Jul 3, 2026
1d1abef
simplify type
tellthemachines Jul 3, 2026
334f202
remove dead schema
tellthemachines Jul 3, 2026
b782190
add missing path
tellthemachines Jul 3, 2026
c5aa21c
add changelog
tellthemachines Jul 3, 2026
5c6a864
ensure single tablet value is exposed as tablet in the UI
tellthemachines Jul 7, 2026
75c7407
fix spacinggg
tellthemachines Jul 7, 2026
1f5523c
add private api changelog
tellthemachines Jul 7, 2026
8953b73
add pr link
tellthemachines Jul 7, 2026
f07ced1
update docs with tablet behaviour
tellthemachines Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backport-changelog/7.1/12395.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/12395

* https://github.com/WordPress/gutenberg/pull/79104
16 changes: 16 additions & 0 deletions docs/how-to-guides/themes/global-settings-and-styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,22 @@ Block styles can be scoped to two named breakpoints: `@mobile` and `@tablet`. An
| `@mobile` | `@media (width <= 480px)` |
| `@tablet` | `@media (480px < width <= 782px)` |

Themes can override the default breakpoint widths with `settings.viewport.mobile` and `settings.viewport.tablet`. Breakpoint values can use `px`, `em`, or `rem` units.
Comment thread
t-hamano marked this conversation as resolved.

If only one valid breakpoint is configured, the breakpoint keeps its viewport key and uses a single max-width query. For example, a theme with only `settings.viewport.tablet` will expose `@tablet` styles using `@media (width <= value)`.

```json
{
"version": 3,
"settings": {
"viewport": {
"mobile": "640px",
"tablet": "960px"
}
}
}
```

Responsive overrides can be placed directly on a block node:

```json
Expand Down
11 changes: 11 additions & 0 deletions docs/reference-guides/theme-json-reference/theme-json-living.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,17 @@ Settings related to typography.

---

### viewport

Viewport breakpoint settings used for responsive block styles and block visibility. Invalid breakpoint values are ignored. If only one valid breakpoint is declared, it remains keyed by its configured viewport and uses a single max-width query. If neither breakpoint is valid, the default breakpoints are used. If `mobile` is greater than or equal to `tablet`, only the `mobile` breakpoint is used.

| Property | Description | Type | Default |
| -------- | ----------- | ---- | ------- |
| mobile | Maximum viewport width for mobile styles. | `string` | `"480px"` |
| tablet | Maximum viewport width for tablet styles. Tablet styles apply above the mobile width and up to this width when both breakpoints are configured, or up to this width when tablet is the only valid breakpoint. | `string` | `"782px"` |

---

### custom

Generate custom CSS custom properties of the form `--wp--custom--{key}--{nested-key}: {value};`. `camelCased` keys are transformed to `kebab-case` as to follow the CSS property naming schema. Keys at different depth levels are separated by `--`, so keys should not include `--` in the name.
Expand Down
5 changes: 5 additions & 0 deletions lib/block-supports/block-style-variations.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block )
),
);

// Ensure variation state styles know about any custom viewport breakpoints.
if ( isset( $theme_json['settings']['viewport'] ) ) {
$config['settings']['viewport'] = $theme_json['settings']['viewport'];
}

// Turn off filter that excludes block nodes. They are needed here for the variation's inner block types.
if ( ! is_admin() ) {
remove_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' );
Expand Down
60 changes: 12 additions & 48 deletions lib/block-supports/block-visibility.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,60 +42,24 @@ function gutenberg_render_block_visibility_support( $block_content, $block ) {
return $block_content;
}

/*
* Viewport size definitions are in several places in WordPress packages.
* The following are taken from: https://github.com/WordPress/gutenberg/blob/trunk/packages/base-styles/_breakpoints.scss
* The array is in a future, potential JSON format, and will be centralized
* as the feature is developed.
*
* Viewport sizes as array items are defined sequentially. The first item's size is the max value.
* Each subsequent item starts after the previous size (using > operator), and its size is the max.
* The last item starts after the previous size (using > operator), and it has no max.
*/
$viewport_sizes = array(
array(
'name' => 'Mobile',
'slug' => 'mobile',
'size' => '480px',
),
$viewport_settings = gutenberg_get_global_settings( array( 'viewport' ) );
$viewport_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries(
$viewport_settings,
array(
'name' => 'Tablet',
'slug' => 'tablet',
'size' => '782px',
),
array(
'name' => 'Desktop',
'slug' => 'desktop',
/*
* Note: the last item in the $viewport_sizes array does not technically require a 'size' key,
* as the last item's media query is calculated using `width > previous size`.
* The last item is present for validating the attribute values, and in order to indicate
* that this is the final viewport size, and to calculate the previous media query accordingly.
*/
),
'include_desktop' => true,
)
);

/*
* Build media queries from viewport size definitions using the CSS range syntax.
* Could be absorbed into the style engine,
* as well as classname building, and declaration of the display property, if required.
* Viewport media queries are keyed by style-state names (`@mobile`,
* `@tablet`, and `@desktop`). Block visibility metadata and generated
* classes use plain viewport names, so map the keys at this boundary.
*/
$viewport_media_queries = array();
$previous_size = null;
foreach ( $viewport_sizes as $index => $viewport_size ) {
// First item: width <= size.
if ( 0 === $index ) {
$viewport_media_queries[ $viewport_size['slug'] ] = "@media (width <= {$viewport_size['size']})";
} elseif ( count( $viewport_sizes ) - 1 === $index && $previous_size ) {
// Last item: width > previous size.
$viewport_media_queries[ $viewport_size['slug'] ] = "@media (width > $previous_size)";
} else {
// Middle items: previous size < width <= size.
$viewport_media_queries[ $viewport_size['slug'] ] = "@media ({$previous_size} < width <= {$viewport_size['size']})";
}

$previous_size = $viewport_size['size'] ?? null;
$block_visibility_media_queries = array();
foreach ( $viewport_media_queries as $viewport_state => $media_query ) {
$block_visibility_media_queries[ ltrim( $viewport_state, '@' ) ] = $media_query;
}
$viewport_media_queries = $block_visibility_media_queries;

$hidden_on = array();

Expand Down
16 changes: 9 additions & 7 deletions lib/block-supports/layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -955,19 +955,22 @@ function gutenberg_unique_id_from_values( array $data, string $prefix = '' ): st
function gutenberg_render_layout_support_flag( $block_content, $block ) {
static $global_styles = null;

$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
$block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
$style_attr = gutenberg_resolve_style_state_aliases(
$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] );
$block_supports_layout = block_has_support( $block_type, array( 'layout' ), false ) || block_has_support( $block_type, array( '__experimentalLayout' ), false );
$style_attr = gutenberg_resolve_style_state_aliases(
$block['attrs']['style'] ?? array(),
$block['blockName']
);
$global_settings = gutenberg_get_global_settings();
$viewport_settings = $global_settings['viewport'] ?? null;
$responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings );
// If there is any value in style -> layout, the block has a child layout.
$child_layout = $style_attr['layout'] ?? null;

// Collect responsive viewport child layout overrides so that a block with
// only responsive child layout (no base child layout) is still processed.
$viewport_child_layouts = array();
foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
$viewport_child = gutenberg_get_layout_child_values( $style_attr[ $breakpoint ]['layout'] ?? null );
if ( ! empty( $viewport_child ) ) {
$viewport_child_layouts[ $breakpoint ] = array(
Expand Down Expand Up @@ -1077,7 +1080,6 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
return $block_content;
}

$global_settings = gutenberg_get_global_settings();
$fallback_layout = $block_type->supports['layout']['default'] ?? array();
if ( empty( $fallback_layout ) ) {
$fallback_layout = $block_type->supports['__experimentalLayout']['default'] ?? array();
Expand Down Expand Up @@ -1189,7 +1191,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
$block_spacing,
);

foreach ( array_keys( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) {
foreach ( array_keys( $responsive_media_queries ) as $breakpoint ) {
$viewport_style = $style_attr[ $breakpoint ] ?? null;
if ( ! is_array( $viewport_style ) ) {
continue;
Expand Down Expand Up @@ -1235,7 +1237,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) {
* Emit responsive container layout styles using the same $container_class
* selector as the base layout so they target the inner block wrapper.
*/
foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
$viewport_style = $style_attr[ $breakpoint ] ?? null;
if ( ! is_array( $viewport_style ) ) {
continue;
Expand Down
10 changes: 6 additions & 4 deletions lib/block-supports/states.php
Original file line number Diff line number Diff line change
Expand Up @@ -513,12 +513,14 @@ function gutenberg_render_block_states_support( $block_content, $block ) {
return $block_content;
}

$supported_pseudo_states = WP_Theme_JSON_Gutenberg::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array();
$style = gutenberg_resolve_style_state_aliases(
$supported_pseudo_states = WP_Theme_JSON_Gutenberg::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ?? array();
$style = gutenberg_resolve_style_state_aliases(
$block['attrs']['style'] ?? array(),
$block_name
);
$css_rules = array();
$css_rules = array();

@talldan talldan Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How I hate this = alignment lint rule. 馃槅

Ok, "hide whitespace" now active for the rest of the review.

$viewport_settings = gutenberg_get_global_settings( array( 'viewport' ) );
$responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings );

foreach ( $supported_pseudo_states as $pseudo_state ) {
if ( empty( $style[ $pseudo_state ] ) || ! is_array( $style[ $pseudo_state ] ) ) {
Expand All @@ -534,7 +536,7 @@ function gutenberg_render_block_states_support( $block_content, $block ) {
);
}

foreach ( WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS as $breakpoint => $media_query ) {
foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
if ( empty( $style[ $breakpoint ] ) || ! is_array( $style[ $breakpoint ] ) ) {
continue;
}
Expand Down
Loading
Loading