Modal

A dialog that opens over the page, loaded from the server.

Default
{ui button,
	title: 'Delete page',
	variant: 'destructive',
	attrs: [
		'hx-get' => "{$page->url()}:modal",
		'hx-swap' => 'none',
	],
/}

A modal is not a hidden element that a script reveals. It is a fragment the server returns, filled into an empty region of the layout, so the server decides what is inside and nothing about it is stored in the browser.

The layout holds one <div id="modal"></div>. The trigger is a request for a :block of the page, and that block wraps the modal in {fragment 'modal', target: 'modal'}, so the response lands in the region and the trigger needs only hx-swap="none". The empty {else} matters: without it the fragment also renders in the page itself, and the modal would show on every visit.

{ui button,
	title: 'Delete page',
	variant: 'destructive',
	attrs: [
		'hx-get' => "{$page->url()}:modal",
		'hx-swap' => 'none',
	],
/}

{fragment 'modal', target: 'modal'}
	{component modal, title: 'Delete this page?'}
		The page and everything below it will be removed.
	{/component}
{else}
{/fragment}

This is what the server returns for the button above:

{component modal,
	title: 'Delete this page?',
	description: 'This cannot be undone.',
}
	The page and everything below it will be removed.
	{block footer}
		{ui button,
			title: 'Cancel',
			variant: 'outline',
			attrs: ['data-modal-close' => true],
		/}
		{ui button,
			title: 'Delete',
			variant: 'destructive',
			attrs: ['data-modal-close' => true],
		/}
	{/block}
{/component}

Closing and focus

The shared ui.js does what CSS cannot. When a modal appears it moves focus to the dialog, keeps Tab and Shift+Tab inside it, and stops the page behind from scrolling. Escape, a click on the backdrop and any element with data-modal-close remove it, and focus goes back to the button that opened it. The close button in the corner is always there.

To close it after a save, respond with a swap that empties the region, as any other fragment does. The dialog is a div with role="dialog" and aria-modal, not the native dialog element, whose showModal() is newer than the browsers the UI supports.

size is sm or lg and sets --modal-width, which is 32rem by default.

Params

Param Default Description
title null the heading, which also names the dialog
description null muted text under the title
body null the body as a string, or the tag's content
footer null the footer as a string, or {block footer}
size null sm or lg
class '' added to modal
attrs [] any other attributes, e.g. ['id' => 'confirm']