diff --git a/mu-plugins/blocks/modal/index.php b/mu-plugins/blocks/modal/index.php
new file mode 100644
index 00000000..241be329
--- /dev/null
+++ b/mu-plugins/blocks/modal/index.php
@@ -0,0 +1,62 @@
+ false,
+];
+
+// Set up a unique ID for this modal.
+$html_id = wp_unique_id( 'modal-' );
+
+?>
+
$style ]); // phpcs:ignore ?>
+ data-wp-interactive="wporg/modal"
+ data-wp-watch="callbacks.init"
+ data-wp-on--keydown="actions.handleKeydown"
+ data-wp-class--is-modal-open="context.isOpen"
+
+>
+
+
+
+
diff --git a/mu-plugins/blocks/modal/src/block.json b/mu-plugins/blocks/modal/src/block.json
new file mode 100644
index 00000000..ce00b178
--- /dev/null
+++ b/mu-plugins/blocks/modal/src/block.json
@@ -0,0 +1,60 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "wporg/modal",
+ "title": "Modal",
+ "icon": "location",
+ "category": "layout",
+ "description": "A modal dialog.",
+ "textdomain": "wporg",
+ "attributes": {
+ "backgroundColor": {
+ "type": "string",
+ "default": "white"
+ },
+ "customBackgroundColor": {
+ "type": "string"
+ },
+ "textColor": {
+ "type": "string",
+ "default": "charcoal-1"
+ },
+ "customTextColor": {
+ "type": "string"
+ },
+ "closeButtonColor": {
+ "type": "string",
+ "default": "charcoal-1"
+ },
+ "customCloseButtonColor": {
+ "type": "string"
+ },
+ "overlayColor": {
+ "type": "string"
+ },
+ "customOverlayColor": {
+ "type": "string",
+ "default": "#1e1e1ecc"
+ },
+ "href": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string",
+ "default": "Open modal"
+ },
+ "buttonStyle": {
+ "type": "string",
+ "default": ""
+ }
+ },
+ "supports": {
+ "align": false,
+ "layout": false,
+ "interactivity": true
+ },
+ "editorScript": "file:./index.js",
+ "style": "file:./style-index.css",
+ "viewScriptModule": "file:./view.js",
+ "render": "file:../render.php"
+}
diff --git a/mu-plugins/blocks/modal/src/index.js b/mu-plugins/blocks/modal/src/index.js
new file mode 100644
index 00000000..f4606217
--- /dev/null
+++ b/mu-plugins/blocks/modal/src/index.js
@@ -0,0 +1,227 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import {
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- experimental ok.
+ __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown,
+ InnerBlocks,
+ InspectorControls,
+ RichText,
+ useBlockProps,
+ // eslint-disable-next-line @wordpress/no-unsafe-wp-apis -- experimental ok.
+ __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients,
+ withColors,
+} from '@wordpress/block-editor';
+import { PanelBody, SelectControl, TextControl, ToggleControl } from '@wordpress/components';
+import { store as blocksStore, registerBlockType } from '@wordpress/blocks';
+import { useSelect } from '@wordpress/data';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { useState } from 'react';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+import './style.scss';
+
+function Edit( {
+ attributes,
+ setAttributes,
+ backgroundColor,
+ setBackgroundColor,
+ textColor,
+ setTextColor,
+ closeButtonColor,
+ setCloseButtonColor,
+ overlayColor,
+ setOverlayColor,
+ clientId,
+} ) {
+ const [ isModalPreview, setIsModalPreview ] = useState( false );
+ const [ buttonStyleOptions, setButtonStyleOptions ] = useState( [] );
+ const { customBackgroundColor, customCloseButtonColor, customTextColor, customOverlayColor } = attributes;
+ const colorGradientSettings = useMultipleOriginColorsAndGradients();
+
+ useSelect( ( select ) => {
+ const { getBlockStyles } = select( blocksStore );
+ const styles = getBlockStyles( 'core/button' );
+ const options = styles.map( ( item ) => ( { label: item.label, value: item.name } ) );
+ // Add the same options with the `is-small` modifier.
+ styles.forEach( ( item ) => {
+ options.push( { label: `${ item.label } (small)`, value: `${ item.name } is-small` } );
+ } );
+ setButtonStyleOptions( options );
+ }, [] );
+
+ const classes = [];
+ if ( isModalPreview ) {
+ classes.push( 'is-modal-open' );
+ }
+
+ const style = {
+ '--wp--custom--wporg-modal--color--background': backgroundColor.slug
+ ? `var( --wp--preset--color--${ backgroundColor.slug } )`
+ : customBackgroundColor,
+ '--wp--custom--wporg-modal--color--text': textColor.slug
+ ? `var( --wp--preset--color--${ textColor.slug } )`
+ : customTextColor,
+ '--wp--custom--wporg-modal--color--close-button': closeButtonColor.slug
+ ? `var( --wp--preset--color--${ closeButtonColor.slug } )`
+ : customCloseButtonColor,
+ '--wp--custom--wporg-modal--color--overlay': overlayColor.slug
+ ? `var( --wp--preset--color--${ overlayColor.slug } )`
+ : customOverlayColor,
+ };
+
+ const blockProps = useBlockProps( {
+ className: classes,
+ style: style,
+ } );
+
+ return (
+ <>
+
+ {
+ setBackgroundColor( value );
+ setAttributes( {
+ customBackgroundColor: value,
+ } );
+ },
+ },
+ {
+ label: __( 'Modal text', 'wporg' ),
+ colorValue: textColor.color || customTextColor,
+ onColorChange: ( value ) => {
+ setTextColor( value );
+ setAttributes( {
+ customTextColor: value,
+ } );
+ },
+ },
+ {
+ label: __( 'Close button', 'wporg' ),
+ colorValue: closeButtonColor.color || customCloseButtonColor,
+ onColorChange: ( value ) => {
+ setCloseButtonColor( value );
+ setAttributes( {
+ customCloseButtonColor: value,
+ } );
+ },
+ },
+ {
+ label: __( 'Overlay', 'wporg' ),
+ colorValue: overlayColor.color || customOverlayColor,
+ onColorChange: ( value ) => {
+ setOverlayColor( value );
+ setAttributes( {
+ customOverlayColor: value,
+ } );
+ },
+ enableAlpha: true,
+ },
+ ] }
+ panelId={ clientId }
+ hasColorsOrGradients={ false }
+ disableCustomColors={ false }
+ __experimentalIsRenderedInSidebar
+ { ...colorGradientSettings }
+ />
+
+
+
+ {
+ setIsModalPreview( newValue );
+ } }
+ />
+ setAttributes( { href } ) }
+ />
+ {
+ setAttributes( { buttonStyle: newValue } );
+ } }
+ value={ attributes.buttonStyle }
+ options={ buttonStyleOptions }
+ />
+
+
+
+
+
+ setAttributes( { label } ) }
+ placeholder={ __( 'Open modal', 'wporg' ) }
+ />
+
+
+
+
+ setIsModalPreview( false ) }
+ aria-label={ __( 'Close modal', 'wporg' ) }
+ >
+
+
+
+
+ >
+ );
+}
+
+registerBlockType( metadata.name, {
+ edit: withColors( {
+ backgroundColor: 'background-color',
+ textColor: 'text-color',
+ closeButtonColor: 'close-button-color',
+ overlayColor: 'overlay-color',
+ } )( Edit ),
+ save: () => {
+ return ;
+ },
+} );
diff --git a/mu-plugins/blocks/modal/src/style.scss b/mu-plugins/blocks/modal/src/style.scss
new file mode 100644
index 00000000..1e2e8fb6
--- /dev/null
+++ b/mu-plugins/blocks/modal/src/style.scss
@@ -0,0 +1,73 @@
+:where(.wp-block-wporg-modal) {
+ --wp--custom--wporg-modal--color--background: var(--wp--preset--color--white);
+ --wp--custom--wporg-modal--color--text: var(--wp--preset--color--charcoal-1);
+ --wp--custom--wporg-modal--color--overlay: #1e1e1ecc;
+ --wp--custom--wporg-modal--color--close-button: var(--wp--preset--color--charcoal-1);
+}
+
+.wp-block-wporg-modal {
+ position: relative;
+
+ &.is-modal-open {
+ z-index: 100000; /* admin bar + 1. */
+
+ & .wporg-modal__modal {
+ display: block;
+ }
+ }
+}
+
+.wporg-modal__modal-backdrop {
+ position: fixed;
+ inset: 0;
+ height: 100vh;
+ width: 100vw;
+ padding: 20px;
+
+ &:not([hidden]) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--wp--custom--wporg-modal--color--overlay);
+ }
+}
+
+.wporg-modal__modal {
+ display: none;
+ position: relative;
+ width: var(--wp--custom--layout--content-size, 680px);
+ background: var(--wp--custom--wporg-modal--color--background);
+ color: var(--wp--custom--wporg-modal--color--text);
+}
+
+.wporg-modal__modal-close {
+ position: absolute;
+ top: var(--wp--preset--spacing--20);
+ right: var(--wp--preset--spacing--20);
+ z-index: 1;
+ width: 24px;
+ height: 24px;
+ cursor: pointer;
+ border: none;
+ background: transparent;
+ color: var(--wp--custom--wporg-modal--color--close-button);
+
+ &::before {
+ content: "";
+ display: inline-block;
+ position: absolute;
+ inset: 0;
+ /* stylelint-disable-next-line function-url-quotes */
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' aria-hidden='true' focusable='false'%3E%3Cpath d='m13.06 12 6.47-6.47-1.06-1.06L12 10.94 5.53 4.47 4.47 5.53 10.94 12l-6.47 6.47 1.06 1.06L12 13.06l6.47 6.47 1.06-1.06L13.06 12Z'%3E%3C/path%3E%3C/svg%3E");
+ mask-repeat: no-repeat;
+ mask-position: center;
+ background-color: currentcolor;
+ }
+}
+
+.wporg-modal__modal-content {
+ margin: 0;
+ max-height: 80dvh;
+ overflow-y: auto;
+ border: none;
+}
diff --git a/mu-plugins/blocks/modal/src/view.js b/mu-plugins/blocks/modal/src/view.js
new file mode 100644
index 00000000..1514d608
--- /dev/null
+++ b/mu-plugins/blocks/modal/src/view.js
@@ -0,0 +1,93 @@
+/**
+ * WordPress dependencies
+ */
+import { getContext, getElement, store } from '@wordpress/interactivity';
+
+// See https://github.com/WordPress/gutenberg/blob/37f52ae884a40f7cb77ac2484648b4e4ad973b59/packages/block-library/src/navigation/view-interactivity.js
+const focusableSelectors = [
+ 'a[href]',
+ 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
+ 'select:not([disabled]):not([aria-hidden])',
+ 'textarea:not([disabled]):not([aria-hidden])',
+ 'button:not([disabled]):not([aria-hidden])',
+ '[contenteditable]',
+ '[tabindex]:not([tabindex^="-"])',
+];
+
+const { actions } = store( 'wporg/modal', {
+ actions: {
+ toggle: () => {
+ const context = getContext();
+ if ( context.isOpen ) {
+ actions.close();
+ } else {
+ actions.open();
+ }
+ },
+
+ /**
+ * Close the modal only if the backdrop is clicked.
+ * Ignores clicks inside the modal itself.
+ *
+ * @param {Event} event
+ */
+ clickBackdrop: ( event ) => {
+ if ( event.target.classList.contains( 'wporg-modal__modal-backdrop' ) ) {
+ actions.close();
+ }
+ },
+
+ open: () => {
+ const context = getContext();
+ context.isOpen = true;
+ context.modal.focus();
+ },
+
+ close: () => {
+ const context = getContext();
+ context.isOpen = false;
+ context.toggleButton.focus();
+ },
+
+ handleKeydown: ( event ) => {
+ const context = getContext();
+ // Only handle key events if the dropdown is open.
+ if ( ! context.isOpen ) {
+ return;
+ }
+
+ // If Escape close the dropdown.
+ if ( event.key === 'Escape' ) {
+ actions.close();
+ return;
+ }
+
+ // Trap focus.
+ if ( event.key === 'Tab' ) {
+ // If shift + tab it change the direction.
+ if ( event.shiftKey && window.document.activeElement === context.firstFocusableElement ) {
+ event.preventDefault();
+ context.lastFocusableElement.focus();
+ } else if ( ! event.shiftKey && window.document.activeElement === context.lastFocusableElement ) {
+ event.preventDefault();
+ context.firstFocusableElement.focus();
+ }
+ }
+ },
+ },
+
+ callbacks: {
+ init: () => {
+ const context = getContext();
+ const { ref } = getElement();
+ context.toggleButton = ref.querySelector( '.wporg-modal__toggle' );
+ context.modal = ref.querySelector( '.wporg-modal__modal' );
+
+ if ( context.isOpen ) {
+ const focusableElements = context.modal.querySelectorAll( focusableSelectors );
+ context.firstFocusableElement = focusableElements[ 0 ];
+ context.lastFocusableElement = focusableElements[ focusableElements.length - 1 ];
+ }
+ },
+ },
+} );
diff --git a/mu-plugins/loader.php b/mu-plugins/loader.php
index 3d08e7ba..8aacabbf 100644
--- a/mu-plugins/loader.php
+++ b/mu-plugins/loader.php
@@ -37,6 +37,7 @@
require_once __DIR__ . '/blocks/local-navigation-bar/index.php';
require_once __DIR__ . '/blocks/latest-news/latest-news.php';
require_once __DIR__ . '/blocks/link-wrapper/index.php';
+require_once __DIR__ . '/blocks/modal/index.php';
require_once __DIR__ . '/blocks/navigation/index.php';
require_once __DIR__ . '/blocks/notice/index.php';
require_once __DIR__ . '/blocks/query-filter/index.php';