Skip to content

Integrate Resizable Editor with Device Preview and add Responsive editing#75121

Open
t-hamano wants to merge 33 commits into
trunkfrom
integrate-device-preview-resizable-editor
Open

Integrate Resizable Editor with Device Preview and add Responsive editing#75121
t-hamano wants to merge 33 commits into
trunkfrom
integrate-device-preview-resizable-editor

Conversation

@t-hamano

@t-hamano t-hamano commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

Note

This PR originally aimed to integrate Device Preview with the Resizable Editor. However, following discussion, its scope shifted to relocating the Responsive editing UI.

What?

This PR does two things:

  1. Integration — Unifies device view and the resizable editor, which are currently mutually exclusive. In the post and template editors, you could only set the three specific device widths and nothing in between. Meanwhile, in the pattern and navigation editors, you could resize freely but could not fit the canvas to a specific device width.
  2. Relocating the Responsive editing UI — Moves the Responsive editing entry point into the device preview dropdown. Along with this, the responsive (viewport) style state becomes global rather than per-block: switching the device preview (or resizing the canvas) drives which viewport the block style edits in the inspector apply to.

How?

The core idea is to treat the canvas width (in pixels) as the single source of truth, instead of a discrete device type. The device preview dropdown becomes just a tool to set a specific canvas width, and resizing the canvas is the same operation expressed differently.

New private APIs

  • getCanvasWidth() / setCanvasWidth( width ) — Gets/sets the canvas width in pixels. While Responsive editing is enabled, setting it also drives the viewport style state.
  • isResponsiveEditing() / setResponsiveEditing( enabled ) — Gets/toggles whether Responsive editing is enabled (session-only).
  • getStyleStateViewport() / setStyleStateViewport( viewport ) — Gets/sets the globally selected viewport style state. Block style edits in the inspector apply to this viewport.

Changed / deprecated APIs

  • getDeviceType() / setDeviceType() (editor) — getDeviceType now derives the device type from canvasWidth instead of a stored deviceType, and setDeviceType converts the device type to a width and dispatches setCanvasWidth rather than storing the device type directly.
  • useResizeCanvas() (block-editor) — Deprecated and turned into a no-op. This hook only existed for the old device view, which canvas width now replaces.
  • updateDeviceTypeForViewportState() (editor, private) — Removed. The viewport ↔ device-preview sync now lives inside setCanvasWidth.

Testing Instructions

Post Editor, Template Editor

  • Please note that the resize handles will not be visible initially when the canvas width corresponds to the Desktop view. This is because there is not enough space on the left or right side of the canvas for them to appear. This may be addressed in a follow-up PR. See Allow access to responsive canvas resizing beyond just for template parts #71210 (comment). In the post editor and template editor, you will need to use the device preview dropdown first.
  • In mobile or tablet width, expand the resize handles to their fullest extent. The resize handles should disappear and you should switch to desktop view.
4dd92e2f8dde1b20e416c7c63516bf3e.mp4

Pattern Editor

The resize handles are always visible in this editor. Make sure the device preview dropdown and canvas width work together nicely.

7f71a97bacedc7e0526cc396311b7b28.mp4

Responsive editing UI

  • Open the device preview dropdown and enable Responsive editing.
  • Select a block that supports styles. A viewport badge should appear in the block inspector.
  • Switch the device preview to Tablet or Mobile (or resize the canvas to the corresponding width). The viewport badge should follow the current device.
  • Change a style and confirm the edit applies only to the selected viewport, not to Desktop.
  • Switch back to Desktop and confirm the per-viewport edits are preserved and shown per device.
  • Disable Responsive editing and confirm the viewport editing state resets to default and the badge disappears.

@github-actions github-actions Bot added [Package] Editor /packages/editor [Package] Block editor /packages/block-editor [Package] Edit Post /packages/edit-post labels Feb 2, 2026
@github-actions

github-actions Bot commented Feb 2, 2026

Copy link
Copy Markdown

Size Change: +529 B (+0.01%)

Total Size: 8.6 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 380 kB -71 B (-0.02%)
build/scripts/edit-post/index.min.js 52.9 kB -20 B (-0.04%)
build/scripts/editor/index.min.js 474 kB +354 B (+0.07%)
build/styles/editor/style-rtl.css 30.9 kB +68 B (+0.22%)
build/styles/editor/style-rtl.min.css 26.3 kB +63 B (+0.24%)
build/styles/editor/style.css 30.9 kB +71 B (+0.23%)
build/styles/editor/style.min.css 26.3 kB +64 B (+0.24%)

compressed-size-action

<PreviewDropdown
forceIsAutosaveable={ forceIsDirty }
disabled={ disablePreviewOption }
disabled={ isStylesCanvasActive }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The only exception is StyleBook, where the device preview dropdown is not available, and the canvas is not resizable. I plan to remove this limitation in a follow-up.

@t-hamano t-hamano added [Type] Enhancement A suggestion for improvement. General Interface Parts of the UI which don't fall neatly under other labels. labels Feb 2, 2026
@t-hamano t-hamano marked this pull request as ready for review February 2, 2026 03:12
@github-actions

github-actions Bot commented Feb 2, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: t-hamano <wildworks@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: jasmussen <joen@git.wordpress.org>
Co-authored-by: tellthemachines <isabel_brison@git.wordpress.org>
Co-authored-by: stokesman <presstoke@git.wordpress.org>
Co-authored-by: jameskoster <jameskoster@git.wordpress.org>
Co-authored-by: talldan <talldanwp@git.wordpress.org>
Co-authored-by: fcoveram <fcoveram@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@t-hamano t-hamano self-assigned this Feb 2, 2026

@Mamaduka Mamaduka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I haven't tested this thoroughly, but I've left some notes based on the initial review.

I think it's an interesting approach. Can you elaborate a bit on why a new global state is needed rather than just using the previous method?

Comment thread packages/editor/src/components/preview-dropdown/index.js Outdated
Comment thread packages/editor/src/components/resizable-editor/index.js Outdated
Comment thread packages/editor/src/components/visual-editor/index.js Outdated
Comment thread packages/editor/src/utils/device-type.js Outdated
@t-hamano

t-hamano commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

@Mamaduka Thanks for the review!

Can you elaborate a bit on why a new global state is needed rather than just using the previous method?

The reason I introduced the new canvasWidth state is because getDeviceType and setDeviceType are public APIs. I know several plugins depend on them, and those APIs need to continue to be available in the same way as before.

@Mamaduka

Mamaduka commented Feb 2, 2026

Copy link
Copy Markdown
Member

Yes, but what prevents us from using useResizeCanvas and the old system, but only enabling resizing when deviceType !== 'Desktop' and probably some other conditions depending on the post type.

I'm not saying that either option is better; I'm primarily curious about the technical decisions.

@tellthemachines

Copy link
Copy Markdown
Contributor

In resizable editors, getDeviceType always returns Desktop, so the block hiding feature does not work in resizable editors, such as the Pattern Editor and Navigation Editor.

Can you give me some steps to repro this? A quick check of the pattern editor by creating a new pattern, adding a block and selecting "hide on desktop" hides it successfully for me:

Screenshot 2026-02-02 at 4 59 54 pm

We're not only checking the device type but also the viewport size for the block hiding logic, so if it doesn't work in responsive editing there's something wrong with it 😅

@t-hamano

t-hamano commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

In resizable editors, getDeviceType always returns Desktop, so the block hiding feature does not work in resizable editors, such as the Pattern Editor and Navigation Editor.

Can you give me some steps to repro this?

@tellthemachines Can you try the following steps? I tested it on trunk (f2c4004).

  1. Appearance > Editor > Patterns
  2. Create a new pattern
  3. Enter some text into the default Paragraph block.
  4. Open the Hide block modal and check "Hide on Tablet”.
  5. Change the editor width slightly. The block will always be visible.
d0d26373639acd414cd859e8ce4e469a.mp4

@t-hamano

t-hamano commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

Yes, but what prevents us from using useResizeCanvas and the old system, but only enabling resizing when deviceType !== 'Desktop' and probably some other conditions depending on the post type.

@Mamaduka Can you elaborate a bit more on your concerns? I may not be understanding you properly 😅

t-hamano and others added 8 commits June 17, 2026 11:31
…lBadges

Reorder props so viewport-related props (viewportStates, viewportValue)
and pseudo-related props (pseudoStates, pseudoStateValue) are adjacent,
improving readability.

Co-Authored-By: Claude <noreply@anthropic.com>
Align the deprecation notice with the release the no-op change actually
ships in.

Co-Authored-By: Claude <noreply@anthropic.com>
Trim the explanatory comments around the global viewport / per-block
pseudo split to keep them concise.

Co-Authored-By: Claude <noreply@anthropic.com>
The canvasMinHeight action, selector, and reducer were wired into the
store but had no consumers reading or setting them. Drop the dead state
to keep the canvas store surface limited to canvasWidth.

Co-Authored-By: Claude <noreply@anthropic.com>
Group the canvas width reducer with the related rendering-mode state
(where the former deviceType reducer lived) for readability.

Co-Authored-By: Claude <noreply@anthropic.com>
Move the isResponsiveEditing reducer and its combineReducers entry next
to renderingMode/canvasWidth so the related device-preview state stays
together.

Co-Authored-By: Claude <noreply@anthropic.com>
Document that the currently hardcoded DEVICE_TYPES breakpoints are
expected to become customizable via theme.json settings.viewport, so
readers know the literals are a temporary baseline.

Co-Authored-By: Claude <noreply@anthropic.com>
Explain why vertical and horizontal padding now have separate triggers:
vertical frames a width-constrained canvas, horizontal reserves space
for the resize handles.

Co-Authored-By: Claude <noreply@anthropic.com>
t-hamano and others added 3 commits June 17, 2026 12:55
The selector built a fresh object on every call, so useSelect in
BlockStyleControls saw a new selectedState reference each render and
warned about unstable values / unnecessary re-renders. Wrap it in
createSelector keyed on the viewport and per-block style state so the
same inputs return a stable reference.

Co-Authored-By: Claude <noreply@anthropic.com>
Drop the showDeviceBadge and showStyleStateInfo locals and gate the
style-state info area directly on hasPseudoState || isResponsiveEditing.
This removes the style-support guard so the device badge shows for any
block during Responsive editing, matching BlockStateBadges, and resolves
the inconsistency where the outer guard suppressed the badge.

Co-Authored-By: Claude <noreply@anthropic.com>
The unit test selected a viewport state through the per-block "State:
Default" dropdown, which no longer exists: viewport states are now
chosen via the editor's global device preview (Responsive editing),
which lives in @wordpress/editor and cannot be rendered from a
block-library unit test. Driving the block editor store directly would
require leaking the editor's sub-registry through the shared integration
test helper, which is too invasive.

Remove the broken unit test and its now-unused helper, and cover the
behavior with an e2e test that exercises the real user flow: enable
Responsive editing, switch the device preview to Tablet, and assert the
Cover overlay controls are hidden.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the [Package] Block library /packages/block-library label Jun 17, 2026
t-hamano and others added 3 commits June 17, 2026 15:40
Add an info description to the "Responsive editing" menu item clarifying
that edits made in this mode apply only to the current state, so users
understand the scope before enabling it.

Co-Authored-By: Claude <noreply@anthropic.com>
The viewport badge followed the device preview dropdown but not a manual
canvas resize: the dropdown imperatively synced the viewport style state,
while the resize handle only updated the canvas width. Since the canvas
width is the single source of truth for the device preview, centralize
the sync in setCanvasWidth so the viewport style state stays in step
however the width changes, and drop the now-redundant sync from the
preview dropdown handler.

Co-Authored-By: Claude <noreply@anthropic.com>
The viewport is selected globally via the device preview, yet the
per-block selected style state still carried a viewport field. The
reducer seeded it and the inspector wrote it back, but the selector
always overrode it with the global viewport, so the per-block copy was
never read. Stop storing it: remove the reducer seed, and pass only the
changed value from the inspector so the global viewport is not written
back. The selector keeps deriving the viewport from the global state.

Co-Authored-By: Claude <noreply@anthropic.com>
@t-hamano t-hamano changed the title Integrate Resizable Editor with Device Preview Integrate Resizable Editor with Device Preview and add Responsive editing Jun 17, 2026
@t-hamano

Copy link
Copy Markdown
Contributor Author

Sorry for the late reply. I have reviewed all the code, updated the PR description, and addressed the review comments as much as possible. The preview dropdown looks like this.

image

One thing I noticed while working on this is that, even though the viewport state is now global, it is still treated as part of the per-block state, which puts a bit of strain on the implementation. If we truly want to make the viewport setting global, we may need to decouple it from the per-block setting — but that feels like a larger undertaking.

@jasmussen jasmussen left a comment

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.

This is a strong PR, nice work. I'll defer to others on other details or followups, but for me this looks good.

One detail, to fix or ignore, if I go to the site editor and edit a template, I see View site here:

Image

Similar to other screens I would expect that at the bottom, rather than in the middle. Minor.

@tellthemachines

Copy link
Copy Markdown
Contributor

even though the viewport state is now global, it is still treated as part of the per-block state, which puts a bit of strain on the implementation. If we truly want to make the viewport setting global

That's pretty much what makes me uncomfortable with the preview-only approach: it's pretending to be something it isn't. Global state would imply that anything you do in a certain viewport only applies to that viewport, but that's not the case here.

What actually happens is that the state enables some of the inspector controls to output viewport-specific styles. In the context of the inspector that's fairly clear because we're hiding the controls that don't work when the state is enabled, but the UI changes in this PR make it seem that the state is applicable to the whole editor. But the block toolbar controls aren't viewport-specific. If I remove an image caption in the mobile view, it stays removed across all breakpoints. The same goes for reordering blocks and replacing block content.

An additional subtlety is the difference between "Desktop" and "Default": Default state means anything you do applies to all breakpoints. That's what happens when "Desktop" is set in the preview, but with "responsive editing" checked, when we say edits apply only to the current state, that's not true for Desktop.

We're early enough in the 7.1 cycle that I think we can afford to try this out and see how it feels (and hopefully get feedback on it) but we do need to iterate on a few things, or we risk shipping a very confusing experience.

@ramonjd

ramonjd commented Jun 19, 2026

Copy link
Copy Markdown
Member

Global state would imply that anything you do in a certain viewport only applies to that viewport

I'd agree with this. To take it further, the drop down promises "Edits apply only to the current state."

Which edits?

I know it's meant for styles, but does the user? And where are the gaps, that is, things that won't apply like certain block controls etc.

I like the resizable editor - it solves a UX problem, namely that the user can have finer control over previews in the editor, but my gut instinct is that it's too early for the drop down addition until responsive styles is more mature.

That's just my non-blocking opinion. Thanks!

const isBlockStyleStateSelected =
( !! blockType?.attributes?.style &&
hasViewportBlockStyleState( selectedBlockStyleState ) ) ||
hasPseudoBlockStyleState( selectedBlockStyleState );

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.

isDefaultBlockStyleState just calls these two functions, so we shouldn't need to replace it here.

What does blockType?.attributes?.style tell us about block style state selection?

deprecated( 'wp.blockEditor.useResizeCanvas', {
since: '7.1',
hint: 'This hook is deprecated and no longer does anything.',
} );

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.

Given this is exported from block editor components and could potentially be in use by third parties should we do this? Is there at least an alternative function that we can suggest to replace it?


return state.selectedBlockStyleState.value ?? DEFAULT_BLOCK_STYLE_STATE;
}
export const getSelectedBlockStyleState = createSelector(

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.

Is this change necessary? I don't see the viewport value being used anywhere in this PR. The function name being getSelectedBlockStyleState I'd expect it to be concerned with a specific block, not the wider state value (even if they are the same)

@jasmussen

Copy link
Copy Markdown
Contributor

An additional subtlety is the difference between "Desktop" and "Default": Default state means anything you do applies to all breakpoints. That's what happens when "Desktop" is set in the preview, but with "responsive editing" checked, when we say edits apply only to the current state, that's not true for Desktop.

Yes, this is something to keep a close eye on. We discussed some potentially text only changes to mitigate it here, and there's some additional discussion here. Unless I'm misunderstanding!

@tellthemachines

Copy link
Copy Markdown
Contributor

We discussed some potentially text only changes to mitigate it here, and there's some additional discussion here. Unless I'm misunderstanding!

Oh I see, I missed your comment of 2 weeks ago, sorry!

I'm not sure about Desktop-only edits. Would they be useful? If we went that route it would still be possible to style defaults over all breakpoints by switching off responsive editing. It would also enable, to some extent, a mobile-first approach 😄 in that styles could default to a mobile layout and then add tablet/desktop styles on top.

Our whole approach to the editor so far has been Desktop-first though, to the extent that it's not even possible to use these tools (device preview and block inspector) on a phone. There might be additional complications to enabling this that I'm not seeing right now. Would be keen to hear what other folks think!

@jasmussen

Copy link
Copy Markdown
Contributor

There were 52 collapsed comments, nothing to be sorry for, I just appreciate Aki's patience and resilience with this PR.

I'm not sure about Desktop-only edits. Would they be useful?

No, I don't think they would.

What I think is useful to be clear-eyed about, is the philosophy for responsiveness that we are establishing for the editor, and how we apply that, because there are a few ways this can go:

1: Mobile-first, min-width based

Essentially, mobile styles are default, larger breakpoints override upward using min-width. E.g.

/* Tablet and up */
@media (min-width: 768px) {
  .card {
    display: flex;
    padding: 1.5rem;
    font-size: 1rem;
  }
}

2: Desktop-first, max-width based

Desktop styles are default, smaller breakpoints override downwards using max-width. E.g.

/* Tablet and down */
@media (max-width: 1023px) {
  .card {
    padding: 1.5rem;
    font-size: 1rem;
  }
}

3: Strictly gated ranges, with an optional separate default layer

Each breakpoint adds both min and max-width values, which lock their customizations to that breakpoint only. E.g.

/* Tablet only */
@media (min-width: 768px) and (max-width: 1023px) {
  .card {
    display: flex;
    padding: 1.5rem;
    font-size: 1rem;
  }
}

There are pros and cons to each, but it seems useful to decide between these three strategies and then apply it in a uniform way. Since the site editor is so desktop-first, I always thought the most logical way was to cast Desktop as the default style base, and tablet and mobile (as well as any additioal breakpoints users might be able to register though theme.json in the future) were additive in the downwards direction.

@fcoveram

Copy link
Copy Markdown
Contributor

When changing to Tablet or Mobile to see the handles and resize, I don't see the viewport icon while dragging, just when I drop it. The viewport change is visible when some elements change in the Canvas, but if not, it's not possible to know which viewport I'm about to select until I drop the handle.

Here is a recording that I see in both post/template and pattern editor, and with and without the responsive editing mode.

handle.drag-and-drop.interaction.mp4

Regarding the conceptual approach for responsiveness, I second @jasmussen's take

Since the site editor is so desktop-first, I always thought the most logical way was to cast Desktop as the default style base, and tablet and mobile (as well as any additional breakpoints users might be able to register though theme.json in the future) were additive in the downwards direction.

Since editor was built primarily to be used on desktop, it's fair to assume that desktop-first is the way of editing a site. But there are also solid reasons to think that mobile-first is a common use case of editing. For that, a responsive editing settings that allow to push downwards or upwards the edits made (from desktop to the bottom/table to the bottom, or mobile to the top/tablet to the top) at the block level and for the whole editor could work in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

General Interface Parts of the UI which don't fall neatly under other labels. [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Package] Edit Post /packages/edit-post [Package] Editor /packages/editor [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow access to responsive canvas resizing beyond just for template parts

10 participants