Skip to content

New Block: core/dialog#71618

Open
sethrubenstein wants to merge 44 commits intoWordPress:trunkfrom
pewresearch:core/dialog
Open

New Block: core/dialog#71618
sethrubenstein wants to merge 44 commits intoWordPress:trunkfrom
pewresearch:core/dialog

Conversation

@sethrubenstein
Copy link
Contributor

@sethrubenstein sethrubenstein commented Sep 11, 2025

What?

Closes #61297

#61297

This PR introduces 3 blocks from prc-block-library.

core/dialog

core/dialog is actually a collection of three blocks: 1. core/dialog a wrapper really for 2. core/dialog-trigger, and 3. core/dialog-element.

core/dialog-trigger

This block is the "trigger" or "action" to open the dialog-element <dialog/> block.

core/dialog-element

This is the primary block and is a representation of the <dialog/> element in modal form. With added support for positioning center, top left, top center, top right, center left, center right, bottom left, center, and right and support for backdrop coloring it utilize core supports for box shadow and border quite well.

Additionally, this block and it's iAPI store have been designed to be maximally extensible. We utilize this block throughout pewresearch.org, most complexly in RLS https://www.pewresearch.org/religious-landscape-study/age-distribution/18-29/?dialogId=dialog_prayer-frequency&activeChartId=6ac4c46314ff76e21be70e1f66fe1a19. This link shows off another feature built into core/dialog, in this case a modified version of it's built in Deep Linking feature. Which allows you to open a dialog on page render so long as it's id is in the url with ?dialogId. There is also an Auto Activation timer for opening a dialog immediately on page render based on a ms timer. Lastly, other plugins and consumers of this block can open/close or close all dialogs utilizing the Interactivity API, like so: store('core/dialog').state.dialogs.[{the dialog's elm id}].isOpen = true or of course identifying all dialogs on page via store('core/dialog').state.dialogs.

Lastly, there is a block binding provided to associate a heading with the dialogLabel attribute for core/dialog-element.
This provides an easy and familiar interface to provide a standard paradigm ~ a heading at the top of a dialog while also allowing the user to set the label and remove the display block, the heading, from the dialog. In either case, if a heading is not present a dynamic heading is created for accessibility and hidden from view on the frontend.

Why?

Dialogs, or "modals" are a common UI pattern that many 3rd parties have attempted. This provides a base and a representation of an actual HTML element, <dialog/> that should be present in the core block library.

How?

Testing Instructions

  1. Enable experimental blocks from Gutenberg settings.
  2. Open a page/post and insert a Dialog Block
  3. Add content to the "trigger"
  4. Click "edit dialog" in the block toolbar
  5. The dialog should open in the editor
  6. Add some content and save the post
  7. Preview post and click on your trigger
  8. Your dialog should appear

Testing Instructions for Keyboard

The block toolbar controls should allow keyboard accessibility to open/close dialogs in the editor. And on the frontend the trigger is wrapped with a button element with proper arias to signal relationships. In both contexts there are keyboard handlers to handle escaping out of a dialog.

Screenshots or screencast

CleanShot.2025-09-11.at.13.24.28.mp4
CleanShot.2025-09-11.at.13.29.24.mp4

@github-actions
Copy link

github-actions bot commented Sep 11, 2025

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.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @Dionnie.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

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

Unlinked contributors: Dionnie.

Co-authored-by: sethrubenstein <smrubenstein@git.wordpress.org>
Co-authored-by: luisherranz <luisherranz@git.wordpress.org>
Co-authored-by: jameskoster <jameskoster@git.wordpress.org>
Co-authored-by: jasmussen <joen@git.wordpress.org>
Co-authored-by: joedolson <joedolson@git.wordpress.org>
Co-authored-by: jhmonroe <jhmonroe@git.wordpress.org>
Co-authored-by: paaljoachim <paaljoachim@git.wordpress.org>
Co-authored-by: michalczaplinski <czapla@git.wordpress.org>
Co-authored-by: draganescu <andraganescu@git.wordpress.org>
Co-authored-by: djcowan <djcowan@git.wordpress.org>
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: TheJeffr0 <jeffr0@git.wordpress.org>
Co-authored-by: felixarntz <flixos90@git.wordpress.org>
Co-authored-by: scruffian <scruffian@git.wordpress.org>
Co-authored-by: vk17-starlord <vineet2003@git.wordpress.org>
Co-authored-by: bhubbard <bhubbard@git.wordpress.org>

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

@sethrubenstein sethrubenstein changed the title Core/dialog New Block: core/dialog Sep 11, 2025
@sethrubenstein
Copy link
Contributor Author

Just occurred to me, this will need block icons for all three blocks

@Mamaduka Mamaduka added the New Block Suggestion for a new block label Sep 12, 2025
Copy link
Member

@luisherranz luisherranz left a comment

Choose a reason for hiding this comment

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

This is awesome, Seth 🙂👏

Lastly, other plugins and consumers of this block can open/close or close all dialogs utilizing the Interactivity API, like so: store('core/dialog').state.dialogs.[{the dialog's elm id}].isOpen = true or of course identifying all dialogs on page via store('core/dialog').state.dialogs.

We can combine private and public stores to make this block extensible without exposing all the internal details of the store publicly. It would be the first one from Core, but I think it would be good to do it so we can start exploring extensibility patterns. We can check it a bit later when the new implementation has advanced a bit more.

Other than that, I've only taken a preliminary look at the Interactivity API part, and in general, it's quite good, although here are a few small suggestions.

@sethrubenstein
Copy link
Contributor Author

I'll be making a few more updates by end of the week, mostly around animations and your suggestions @luisherranz.

@sethrubenstein
Copy link
Contributor Author

sethrubenstein commented Sep 16, 2025

We can combine private and public stores to make this block extensible without exposing all the internal details of the store publicly. It would be the first one from Core, but I think it would be good to do it so we can start exploring extensibility patterns. We can check it a bit later when the new implementation has advanced a bit more.

@luisherranz on this topic, interesting, I would just do like this?

// 1st party
store('core/dialog', {
	state:{... internal derivced state funcs },
	actions: {... internal funcs },
	callbacks: {... internal callbacks }
}, {lock:true});

// Accessible by 3rd parties
const { state } = store('core/dialog', {
	actions:{
		open(id){
			state.dialogs[id].isOpen = true;
		}
	}
}, {lock:false})

@luisherranz
Copy link
Member

luisherranz commented Sep 17, 2025

You need to use a different namespace. So it would be more like this:

const { state, actions } = store( 'core/dialog/private', {
  state: {
    // internal derived state funcs
  },
  actions: {
    // internal funcs
  },
  callbacks: {
    // internal callbacks
  }
}, {
  lock: true
} );

store( 'core/dialog', {
  // We add here the state that we want to be public.
  state: {
    // We can use getters for the state that we want to be read-only.
    get dialog() {
      return state.dialog;
    },
    // We can use setters for the state that we want to be modified.
    get isOpen() {
      return state.dialog.isOpen;
    },
    set isOpen( value ) {
      state.dialog.isOpen = value;
    },
  },
  // We add here the actions that we want to be public.
  actions: {
    open( id ) {
      actions.open( id );
    }
  }
} );

I've given the example of how to modify the private state if necessary, but for the most part, we're going to want the state to be read-only and to be modified through actions on public stores.

EDIT: Oh, to return objects like state.dialog that we don't want to be modified, we have to make a small proxy wrapper. If we find that this is a common pattern, we can add this utility to the Interactivity API itself.

function createReadOnlyProxy( obj ) {
  return new Proxy( obj, {
    get( target, prop ) {
      const value = target[ prop ];
      if ( typeof value === 'object' && value !== null ) {
        return createReadOnlyProxy( value );
      }
      return value;
    },
    set() {
      return false;
    },
    deleteProperty() {
      return false;
    }
  });
}

store( 'core/dialog', {
  state: {
    get dialog() {
      return createReadOnlyProxy( state.dialog );
    },

@sethrubenstein
Copy link
Contributor Author

Oh one other thing, I think I'm going to abstract out the dialog close button into it's own block so it can be more easily changed by 3rd parties.

@sethrubenstein
Copy link
Contributor Author

Actually, on further thought, I think I'm going to try to adopt core/icon #71227 in some way for the close button. Stay tuned.

@sethrubenstein
Copy link
Contributor Author

sethrubenstein commented Sep 25, 2025

Some updates from me:

  1. Implemented @luisherranz review on iAPI store including private instance. During the review phase I've also implemented a simple WP_DEBUG conditional button on the dialog trigger that tests 3rd party interactions with this new store method, I'll pull that after a little additional testing.
  2. I have changed how id's are handled, slightly. Instead of another attribute and another ui control I've tied the ID to the parent dialog block's anchor id. If one is not set we'll generate a unique id dynamically and add it to the element and into block context so the child blocks can utilize it accordingly.
  3. Will look into bringing in core/icon for the close button next week.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One thing I wasn't sure about (although I've found it critical to a good experience in the editor) is introducing a custom redux store for a block. I don't believe I've seen a core block do this before. Any thoughts, concerns against this pattern emerging?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going to replace this soon, just familiarizing myself with some recent updates to block attributes around role:local which will allow me to use a core method instead of a new redux store to manage this.

@sethrubenstein
Copy link
Contributor Author

Once we're done with #69789 I'll turn my attention back to this one and finding alignment with #71227

@priethor priethor added the [Type] Feature New feature to highlight in changelogs. label Oct 29, 2025
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Based on some feedback from @t-hamano in core/tabs on feasibility of maintaining these custom style engine implementations long term I'm rethinking how to handle the backdrop for dialogs. Once I wrap up this tabs refactor I'm going to come back in and create a new core/dialog-backdrop block that actually wraps the core/dialog-element block. From there background, alignment, justification, etc will make more sense and fall in line with core block use of style supports.

@jameskoster
Copy link
Contributor

This is really cool to see, nice work so far. There are some design details I think we ought to tighten up before being able to merge:

  • Let's show a warning when deleting an unlocked Dialog trigger or Dialog element. See deleting the Content block for an example.
    • We can advise that folks utilise the Activation Timer Duration option when trigger is removed.
  • I'd consider renaming the "Dialog element" block to just "Dialog".
  • I think it's problematic that the trigger can contain nested interactive elements. Any ideas how to address this?
    • Should we limit which blocks can be inserted in the trigger to begin with? The full set seems excessive.
  • It might be nice if selecting the Dialog element block in List view would toggle the 'Edit dialog' button.
    • Similarly, when the dialog is open in the editor, clicking outside should toggle the 'Edit dialog' button (hide the dialog).
  • If I remove the Dialog element label binding the block throws an error.
  • The "Size" property on Dialog element is a bit confusing. After selecting a size the dialog still seems to grow based on the width of the content. Shouldn't this be a fixed size?
  • It would be good to supply some default styling for Dialog element. Padding and background color specifically.
  • I don't think the animation / styling on the close button adds much value.
  • The close button looks different on the frontend/backend.
  • Could we use the close icon from @wordpress/icons? I'm not sure this needs to be customisable for v1.
  • I'm not sure we need all the animation options. Could we start with the same animation effect as image lightbox?
  • Is the dialog position option necessary? It seems quite niche.

@sethrubenstein
Copy link
Contributor Author

This is really cool to see, nice work so far. There are some design details I think we ought to tighten up before being able to merge:

  • Let's show a warning when deleting an unlocked Dialog trigger or Dialog element. See deleting the Content block for an example.

    • We can advise that folks utilise the Activation Timer Duration option when trigger is removed.
  • I'd consider renaming the "Dialog element" block to just "Dialog".

  • I think it's problematic that the trigger can contain nested interactive elements. Any ideas how to address this?

    • Should we limit which blocks can be inserted in the trigger to begin with? The full set seems excessive.
  • It might be nice if selecting the Dialog element block in List view would toggle the 'Edit dialog' button.

    • Similarly, when the dialog is open in the editor, clicking outside should toggle the 'Edit dialog' button (hide the dialog).
  • If I remove the Dialog element label binding the block throws an error.

  • The "Size" property on Dialog element is a bit confusing. After selecting a size the dialog still seems to grow based on the width of the content. Shouldn't this be a fixed size?

  • It would be good to supply some default styling for Dialog element. Padding and background color specifically.

  • I don't think the animation / styling on the close button adds much value.

  • The close button looks different on the frontend/backend.

  • Could we use the close icon from @wordpress/icons? I'm not sure this needs to be customisable for v1.

  • I'm not sure we need all the animation options. Could we start with the same animation effect as image lightbox?

  • Is the dialog position option necessary? It seems quite niche.

All of these are great points that I'll take into account and address when I pick back up work on this on Monday.

@sethrubenstein
Copy link
Contributor Author

Okay, I think... this is ready for review @annezazu . I (and Opus 4.5 under my direction per https://make.wordpress.org/ai/2026/02/01/ai-guidelines-for-wordpress/) made a number of updates to core/dialog, starting with removal of the custom style engine implementations in favor of the pattern I used in core/tabs, iAPI progressively enhanced static blocks. I also implemented support for the new WP_Icons_Registry (very cool!) along with a filter to allow 3rd parties to customize the close icon. Overall cleanup and removal of dev code.

Per @jameskoster I also took care of these items:

  • Added core/dialog-trigger with some additional logic to filter out core/dialog to BlockRemovalWarnings. This will fire off a warning when the user tries to remove the dialog trigger with guidance to use the auto activation timer instead.
    • In addition I enforced core/dialog-element removal locking via block.json. The block can no longer be deleted.
  • Updated some of the block names to be clearer. core/dialog is now "Dialog Wrapper" and core/dialog-element is now "Dialog. I still think these block names make the most sense semantically.
  • "I think it's problematic that the trigger can contain nested interactive elements. Any ideas how to address this?" Agreed, my best solution thus far is to limit the allowedBlocks via block.json, this will allow 3rd parties to do whatever they want as far as supporting new interior trigger blocks while we support paragraph, heading, image, group (lets users really do whatever they want without having to write a filter to adjust allowedBlocks), and button.
  • Implemented dialog activation when selecting the block in list view, similarly added dialog deactivation to backdrop clicks in the editor.
  • Reduced complexity by removing position and animation options, relying on the animation style from the core/image lightbox.
  • On "* If I remove the Dialog element label binding the block throws an error." I couldn't recreate this?

@sethrubenstein
Copy link
Contributor Author

sethrubenstein commented Feb 6, 2026

I'm considering the image sub-block. It could potentially be interactive with a lightbox now that I think of it. I'll reevaluate some of the sub-block behaviors and clean up any potential interactive elements within the trigger with tag processor tomorrow.

…t when selecting the block either in list view or other means.
…ade it impossible to remove dialog element. Added click handler for backdrop close click in editor.
…name presents as Dialog Wrapper. `core/dialog-element`, now just named Dialog.
…API server side instead of a purely server side block. Removes custom style engine implements. This follows the pattern from my work in core/tabs.
…Icons_Registry for server side close icon in dialog. Notes and style cleanup.
…rovides the glue between editor and client side interactivity and the animation in style.scss.

This also makes it possible for 3rd parties to change the animation, which pivotaly includes the duration. I agree that including a ui for the editor complicates this but this will allow devs to tap into the block more easily.
…jasmussen. Next up, tackling the accessibility issues around dialog trigger...
@sethrubenstein
Copy link
Contributor Author

sethrubenstein commented Feb 21, 2026

@jasmussen, I believe I've addressed all your review concerns. @joedolson, I also conducted a minimal accessibility audit and made some improvements. I would appreciate any feedback you can provide as well.

I removed the "enable deep link" language and changed my original approach of registering a query variable. Instead, I implemented a client-side check for an ID hash in the URL that matches a dialog in state, allowing it to open. This method is cleaner and easier.

Additionally, I have a solution in place for the trigger that wraps p/h/img blocks in a div with the role of a button. However, if the user only has a core buttons block inside, then we should only use that block for the trigger directives, avoiding nested button > button configurations.

Aside from that, I'm not sure what else needs to be done here, if anything. I anticipate some pushback regarding the inclusion of a custom color attribute and controls for the backdrop. I felt that creating another block like core/dialog-backdrop just to pass the background color properly via context into the dialog element would be cumbersome. The only alternative would be to implement official ::backdrop color support specifically for <dialog/> elements, similar to button color support in the style engine. I plan to explore this in a follow-up PR once this is merged.

@jasmussen
Copy link
Contributor

Thanks for working on this, much appreciated. Just a quick GIF to visualize my quick run-through:

dialog

I have more feedback, but I think that should possibly/probably be addressed separately, because with the PR being long and complicated, it's probably best to code-review this and land it as the first iteration. Which is also to say, kudos, nice work!


Some of those details to ponder:

  • Some icon refinements, mainly for the dialog itself. We can likely reuse the "Contents" icon here, same as we did for Tabs and Accordion. It looks like the icons are internal for now, so that's not a blocker.
  • We need a library hover preview example.
  • Should the trigger use the Button block? Are there/should there eventually be other ways to trigger the dialog?
  • What's the relationship between this and the navigation overlay?

Notably the navigation overlay now supports full customization through a template part, so themes can provide the design for the overlay. There are some mockups for overlay customization here, would be nice to share code there.

@jhmonroe
Copy link

jhmonroe commented Feb 23, 2026

Looking great!

In terms of ability to be full width/wide width, is there any talk of sharing some of the implementation of how the navigation overlay template parts are being made?

Apologies if it was covered. I did command-F for "wide, full, width, and 73084" and didn't find.

#73084
https://make.wordpress.org/test/2026/01/27/call-for-testing-customizable-navigation-mobile-overlays/
https://make.wordpress.org/test/2026/01/27/call-for-testing-customizable-navigation-mobile-overlays/#comment-3562

@sethrubenstein
Copy link
Contributor Author

sethrubenstein commented Feb 23, 2026

👋

  • Updated dialog content to use the content icon.
  • Added block example.
  • Revised the dialog template to begin with core/buttons, as this approach is the most logical and commonly used with dialogs. However, I still STRONGLY believe the trigger should support at least core/image and core/paragraph out of the box. It should also allow developers to use existing hooks to adjust allowedBlocks for additional support, such as with video dialogs where we includes a core/media-text block with an image of the video thumbnail in the trigger, and the Dialog Content contains a VideoPress player. Developers can activate a dialog programmatically using iAPI like this: store('core/dialog').state.dialogs.[<block_id>].isOpen = true. We use this method to trigger a dialog without a trigger block for our user survey block. See videos for dialog functioning as a video player and as a user survey element.

https://github.com/user-attachments/assets/fda5ff4a-e2ed-4340-a470-81538cbfc3e6
https://github.com/user-attachments/assets/2b44dbde-2925-4317-90f1-15b176391e96

  • As for Navigation Overlay, I need to dive into the discussion and code here. I would love it if we could share code from that to enhance the editing experience for these modals/dialog. Let me look at whats going on here and get back to you guys in a few days with my thoughts.

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

Labels

New Block Suggestion for a new block [Package] Block library /packages/block-library [Package] Editor /packages/editor [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New Block: Dialog Popup

8 participants