Skip to content

Editor: Add in-editor preview modal - #78599

Open
dhanson-wp wants to merge 1 commit into
WordPress:trunkfrom
dhanson-wp:add/post-page-editor-preview-modal
Open

Editor: Add in-editor preview modal#78599
dhanson-wp wants to merge 1 commit into
WordPress:trunkfrom
dhanson-wp:add/post-page-editor-preview-modal

Conversation

@dhanson-wp

Copy link
Copy Markdown

What?

Adds a new in-editor Preview modal to the post and page editor.

Instead of requiring users to open a separate browser tab to preview content, this introduces a modal preview layer that opens on top of the current editor and preserves editing context.

The modal includes device preview controls for:

  • Desktop
  • Tablet
  • Mobile

The existing external preview flow remains available from the View dropdown as “Preview in new tab”.

Why?

Previewing content currently requires leaving the editing flow, usually through “Preview in new tab”. Device emulation, View Post, and previewing unsaved changes are also mixed together in ways that can be confusing.

Related issues/discussions:

This PR explores a concrete implementation for improving that experience by making preview feel like a layer within the editor rather than a full context switch.

It also creates a potential foundation for future preview extensions, such as:

  • Website preview
  • Email/newsletter preview
  • Social preview
  • Paid subscriber / public visitor preview
  • RSS or print preview
  • Plugin-provided preview modes

How?

This PR introduces a modal-based preview surface that:

  • Opens from the editor preview UI.
  • Displays the current preview route inside the modal.
  • Provides responsive device controls in the modal header.
  • Supports Desktop, Tablet, and Mobile preview sizes.
  • Allows users to close the modal and return to their previous editor context.
  • Keeps the existing external “Preview in new tab” behavior available from the View dropdown.

Screenshots

Desktop preview

Desktop preview

Tablet preview

Tablet preview

Mobile preview

Mobile preview

Testing Instructions

  1. Open the post editor or page editor.
  2. Create or edit a post/page with several blocks.
  3. Click the Preview button in the editor header.
  4. Confirm the preview opens in a modal without navigating away from the editor.
  5. Switch between Desktop, Tablet, and Mobile preview sizes.
  6. Confirm long preview content scrolls inside the modal surface.
  7. Press Escape or use the close button to close the modal.
  8. Confirm focus/context returns to the editor.
  9. Confirm the existing “Preview in new tab” behavior still works from the View dropdown.

Repeat similar checks for:

  • Page editor
  • Post editor

Accessibility

Things to verify:

  • Keyboard focus moves into the modal when it opens.
  • Focus is trapped inside the modal while open.
  • Escape closes the modal.
  • Focus returns to the triggering control after closing.
  • Device controls are keyboard accessible.
  • Modal title/label is announced correctly by screen readers.

Notes / follow-up

This PR does not attempt to solve the entire preview extensibility problem described in #65005, but it may provide a core preview surface that future extensibility work could build on.

Potential follow-up questions:

  • Should plugins be able to register additional preview modes?
  • Should “Preview in new tab” also live inside the modal, or is the existing View dropdown location enough?
  • Should this same modal be used consistently across posts, pages, templates, and the Site Editor?
  • How should this work when previewing templates or full site routes?
  • Should modal previews visually suppress editor/admin chrome such as the WordPress admin bar?

Validation

  • npm run test:unit packages/editor/src/components/post-preview-modal-button/test/index.js
  • npm run lint:js -- packages/editor/src/components/post-preview-modal-button packages/editor/src/components/header/index.js
  • npm run lint:css -- packages/editor/src/components/header/style.scss packages/editor/src/components/post-preview-modal-button/style.scss
  • npm run build
  • Manual browser smoke test in wp-env for opening, switching device sizes, internal scroll, closing with Escape, and preserving the existing external preview option.

@github-actions github-actions Bot added [Package] Editor /packages/editor First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository labels May 23, 2026
@github-actions

Copy link
Copy Markdown

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @dhanson-wp! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@dhanson-wp
dhanson-wp marked this pull request as ready for review May 23, 2026 17:17
@github-actions

github-actions Bot commented May 23, 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: dhanson-wp <dhansondesigns@git.wordpress.org>
Co-authored-by: ramonjd <ramonopoly@git.wordpress.org>
Co-authored-by: simison <simison@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 added the [Type] Enhancement A suggestion for improvement. label May 24, 2026

@ramonjd ramonjd 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.

👋🏻 Thanks for the PR.

I just wanted to link to the PRs/issues below mostly, but it turned into a drive by test 😄

I was going to say, it could be handy to keep an eye on what's happening around configurable breakpoints and responsive block styles.

{ ! isGeneratingPreview && previewUrl && (
<iframe
className={ `editor-post-preview-modal__iframe is-${ device }-preview` }
src={ previewUrl }

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.

Is the admin header useful in the preview?

Image

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not really. I wanted to hide the admin bar, but kept the PR focused on just the main interaction to start.

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.

It's easy to hide with a query arg, so worth doing here:

const src = addQueryArgs( previewUrl, {
	wp_site_preview: 1,
} );

Comment on lines -242 to -246
.editor-header__post-preview-button {
@include break-mobile {
display: none;
}
}

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.

What's the history of this button? Presumably it was only shown in mobile for a reason?

Rather than permanently add this item to the header, maybe we could rethink how to use the current preview buttons.

There are currently 2.

Image

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The current preview buttons are specific for editing and not true previews in context. This PR is intended to replace the Preview in new tab option. I do like making the Preview more prominent, though, because having it in that icon dropdown feels quite hidden. It's not a hard requirement in my mind. We could simply replace the external link and just call it Preview, then it opens the modal.

role="toolbar"
aria-label={ __( 'Preview size' ) }
>
{ DEVICES.map( ( { name, label, icon } ) => (

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.

It might be handy to default to the editor's device preview value (if one is set).

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.

Like so:

const deviceType = useSelect( ( select ) => {
	return select( 'core/editor' ).getDeviceType();
}, [] );

Comment on lines +11 to +25
.editor-post-preview-modal {
.components-modal__content {
display: flex;
flex-direction: column;
overflow: hidden;
padding: 0;
}

.components-modal__children-container {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
}

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.

It would be best not to override component styles like this, and instead use e.g. Stack inside the modal contents.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good call!

Comment on lines +73 to +81
align-items: center;
align-self: stretch;
color: $gray-700;
display: flex;
flex: 1;
flex-direction: column;
gap: $grid-unit-10;
justify-content: center;
text-align: center;

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.

These styles can be replaced by Stack component from @wordpress/ui.

display: flex;
flex: 1;
justify-content: center;
min-height: 0;

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.

You can use Stack component to replace these styles.

Comment on lines +17 to +33
const DEVICES = [
{
name: 'desktop',
label: __( 'Desktop' ),
icon: desktop,
},
{
name: 'tablet',
label: __( 'Tablet' ),
icon: tablet,
},
{
name: 'mobile',
label: __( 'Mobile' ),
icon: mobile,
},
];

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.

Would be good to share these with the preview dropdown so they're not defined twice (plus a few more times in other places).

They're already drifting here to be slightly different for no reason:

/**
* The choices for the device type.
* Duplicated in block-editor block-visibility constants and edit-site
* use-viewport-sync. Update all three when adding new viewport types.
*
* @type {Array}
*/
const choices = [
{
value: 'Desktop',
label: __( 'Desktop' ),
icon: desktop,
},
{
value: 'Tablet',
label: __( 'Tablet' ),
icon: tablet,
},
{
value: 'Mobile',
label: __( 'Mobile' ),
icon: mobile,
},
];


.editor-post-preview-modal__toolbar {
align-items: center;
display: flex;

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.

You can use Stack component to replace these styles.

Comment on lines +150 to +157
<div
className="editor-post-preview-modal__error"
role="alert"
>
{ __(
'The preview could not be generated. Please try again.'
) }
</div>

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.

Could use Notice component from @wordpress/ui here.

{ isGeneratingPreview && (
<div className="editor-post-preview-modal__loading">
<Spinner />
<p>{ __( 'Generating preview…' ) }</p>

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.

Could use Text component from @wordpress/ui here.

const [ isGeneratingPreview, setIsGeneratingPreview ] = useState( false );
const [ previewUrl, setPreviewUrl ] = useState();
const [ previewError, setPreviewError ] = useState();
const [ device, setDevice ] = useState( 'desktop' );

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'm not sure how UX would be impacted, but it would be worth testing how it would feel if the selected device is shared between the dropdown and the modal.

Comment on lines +5 to +8
height: calc(100% - #{ $grid-unit-50 * 2 });
max-height: none;
max-width: none;
width: calc(100% - #{ $grid-unit-50 * 2 });

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.

Do we need these style overrides if you're already setting isFullScreen? Modal also has a size prop in case it helps ("small", "fill", "large", "medium"). Preferably, we don't do component overrides.

@simison simison May 27, 2026

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.

Surprised to see the footer skrim regardless of various overriding styles;
image

Comment on lines +59 to +69
.editor-post-preview-modal__iframe.is-desktop-preview {
width: min(100%, 1200px);
}

.editor-post-preview-modal__iframe.is-tablet-preview {
width: 781px;
}

.editor-post-preview-modal__iframe.is-mobile-preview {
width: 479px;
}

@simison simison May 27, 2026

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.

A bit surprised that these wouldn't come from some shared definition 🤔

If they need to be kept duplicate/separate, would be good to note where else they need to be updated (and vice versa) so that they don't drift.

Comment on lines +112 to +114
<VisuallyHidden render={ <span /> }>
{ __( '(opens in a modal)' ) }
</VisuallyHidden>

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.

Is this accessibility note really needed? I understand it for a new tab but surprised to see it for a modal; I don't think we do that elsewhere in codebase?

<iframe
className={ `editor-post-preview-modal__iframe is-${ device }-preview` }
src={ previewUrl }
title={ __( 'Post preview' ) }

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.

Should this copy adjust based on page/post?

import DocumentTools from '../document-tools';
import HeaderSkeleton from './header-skeleton';
import MoreMenu from '../more-menu';
import PostPreviewButton from '../post-preview-button';

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.

The button is a stable API exported (and documented here) from the package, so it would be important to continue supporting it. Modal version has to be separate component.

@simison
simison requested review from jasmussen and mtias May 27, 2026 08:40
@simison

simison commented May 27, 2026

Copy link
Copy Markdown
Member

I left some comments on implementation, but for the feasibility of this functionality change, I added @mtias and @jasmussen to comment.

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

Labels

First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Package] Editor /packages/editor [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants