Modals
Method’s modals are Bootstrap modals that the page collects while it renders and prints under <body> at the end, so a modal is never trapped in the stacking context of the Section it was authored in. Three parts cooperate: the Modal block (method/modal) that authors write in, the registry (Method_Modals, lib/class-method-modals.php) that collects, de-duplicates and prints them, and a view script (lib/blocks/utils/modals.js) that opens the right modal when any link pointing at its ID is clicked. The registry does not depend on the block, which is what lets a plugin or a child theme register sitewide modals through the same path.
How a modal reaches the page
- Render. The block renders its inner blocks during the normal block render, so anything they enqueue (a form plugin’s script, an embed) is enqueued in time. The render callback then returns an empty string — nothing is printed in place — and hands the rendered content to the registry.
- Register.
Method_Modals::register()normalises the arguments againstdefault_options(), runs themethod/modal_argsfilter, sanitises the ID, and keeps the first registration for an ID (a synced pattern used twice registers the same modal twice; the second firesmethod/modal_duplicateand is dropped). The content goes throughmethod/modal_content, where the usualthe_contentfilters —wptexturize,do_shortcode,wp_filter_content_tagsand the rest — are attached at core’s priorities, because hoisted content never reaches them otherwise. - Print. On
wp_footerat priority 5 the registry prints every modal withget_markup(), and again at priority 19 for anything registered by other footer callbacks; after that it is closed and a latemethod_register_modal()raises_doing_it_wrong(). The view script is enqueued only when at least one modal registered. - Assets. Since WordPress 6.9, assets enqueued by a block that prints nothing are dequeued again. The block opts out through
enqueue_empty_block_content_assets, and the registry also snapshots the script, style and script-module queues around each modal’s render (snapshot_assets()/assets_since_snapshot()) and re-enqueues the difference once the page has rendered. On WordPress 6.5–6.8 the script-module queue is read through a bound closure instead ofget_queue(), which only exists from 6.9. - Open. One delegated click handler opens the
.method-modala link points at —#idor a full URL to the current page, compared by host and path. Links are marked on load withdata-method-modal-trigger,aria-haspopup="dialog",aria-controlsandrole="button"; a URL whose hash is a modal’s ID opens it on load; a link inside one modal closes it before opening the next; media is paused on close and Swipers updated on open.window.methodModals.open( idOrElement, trigger )and.decorate( root )cover content added after load.
Registering a modal from PHP
Any source can register a modal while the page renders, no later than wp_footer priority 18. Render the content first — in the callback, not in a footer hook — so its asset enqueues happen in time. The return value is the ID the modal can be opened with (also when that ID was already taken), or false.
add_action( 'method/register_modals', function ( Method_Modals $registry ) {
$registry->push_context( array( 'scope' => 'site', 'source' => 'child-theme' ) );
method_register_modal( array(
'id' => 'newsletter',
'title' => __( 'Join the newsletter', 'cortlandt' ),
'content' => do_blocks( get_option( 'cortlandt_newsletter_blocks' ) ),
'options' => array(
'size' => 'lg', // sm | default | lg | xl | fullscreen
'fullscreen_below' => 'mobile', // '' | mobile | tablet
'centered' => true,
'close_icon' => 'cross', // a slug from method_theme_icons
),
) );
$registry->pop_context();
} );
// Anywhere in content: <a href="#newsletter">Subscribe</a>
| Argument | Type | Default | Notes |
|---|---|---|---|
id | string | — | Required. Sanitised by method_sanitize_modal_id(): lowercased, runs of other characters become -, prefixed modal- if it does not start with a letter. |
title | string | '' | Plain text. Labels the dialog through aria-labelledby even when hidden; a modal without one gets aria-label="Dialog". |
content | string | '' | Rendered HTML for the body. |
scope | string | post | post or site; set for a batch with push_context() / pop_context(). |
source | string | '' | Free-form origin (block for the block). |
class, css | string | '' | Extra classes on the modal element; CSS scoped to #id.method-modal, collected with the block CSS. |
assets | array | [] | styles, scripts, modules handles to keep enqueued. |
options | array | default_options() | size, fullscreen_below, centered, scrollable, static_backdrop, hide_title, title_tag (h1–h6, p, div; default h2), close_icon, close_label. |
Sizes come from method_get_modal_sizes() — sm 300px, default 500px, lg 800px, xl 1140px, fullscreen — each a label, a .modal-dialog class and a width set as --method-modal-width. default and fullscreen are expected to stay. “Full screen below” a tier adds one shared media-query rule per tier, built from method_get_block_breakpoints().
Hooks
| Hook | Type | Arguments | Use |
|---|---|---|---|
method/register_modals | action | Method_Modals $registry | Fires on wp_enqueue_scripts (priority 5), after the block template has rendered. The place to register sitewide modals. |
method/modal_args | filter | array|false $args | Every modal before registration. Return false to skip it. |
method/modal_content | filter | string $content, array $args | The rendered body. Core’s content filters are attached here at their usual priorities. |
method/modal_markup | filter | string $markup, array $modal | The complete modal HTML before it is printed. |
method/modal_sizes | filter | array $sizes | The size list (slug ⇒ label, class, width). |
method/modal_script_dependencies | filter | array $handles | Dependencies of the view script; default array( 'method' ), the bundle that provides window.bootstrap.Modal. |
method/modal_registered | action | array $args | After a modal is accepted. |
method/modal_duplicate | action | array $args, array $existing | When a registration reuses an ID. The first registration wins. |
The block and the editor
- The block is a hybrid dynamic block like the Container:
savestores only the inner-blocks wrapper (.method-modal-inner-blocks); everything else is attributes —modalId,customId,title,hideTitle,titleTag,size,fullscreenBelow,centered,scrollable,staticBackdrop,closeIcon, colors, andresponsiveSettings. modalId(modal-plus six random characters) is generated the first time the block is edited and never changes; a duplicated or pasted modal takes a fresh one, through theusePersistentIdhook.customIdoverrides it, sanitised bysanitizeModalId()inlib/blocks/utils/modalId.js, the editor twin of the PHP function.- The Modals panel in the document sidebar — Method’s first editor plugin,
method-modal-manager, loaded as the block’s secondeditorScript— lists every modal in the document with title, ID and size, warns about shared IDs, selects a block for editing, copies its link, and appends a new modal. Other sources add their own lists through themethod.modalManager.sectionsJavaScript filter. - Not printed in feeds or REST responses; a block hidden with block visibility does not register.
Styles
Method ships Bootstrap 5.3’s structural modal rules in global.scss, every one wrapped in :where() so they carry no specificity and a child theme’s own .modal rules win regardless of load order. .method-modal adds the fade, the width (--method-modal-width, capped to the viewport), the full-screen size, the close button (button.method-modal-close, 44×44px, rather than Bootstrap’s .btn-close) and margin resets for the first and last inner block. The backdrop fades only while a Method modal is open (html.method-modal-active). Transitions respect prefers-reduced-motion.
Changelog
| Version | Description |
|---|---|
| 2.0.0-beta26 | Introduced: the Modal block, the Modals panel, the Method_Modals registry with method_register_modal(), the hooks above, the view script and the modal styles (e12c9c0). |
| 2.0.0-beta27 | Registry asset snapshot made compatible with WordPress 6.5–6.8, which lack WP_Script_Modules::get_queue(); the block’s effective floor is 6.5 (b0c9c42). |