Skip to content
Home Theme Gallery

Dialog

The custom element is <hui-dialog>.

Overview

A modal panel on the native <dialog>, so the top layer, backdrop, inert page behind and Escape come from the platform. One primitive absorbs Dialog, Sheet and Drawer through side, and Alert Dialog through alert.

Example

Dialog
Source
<hui-dialog>
<hui-button slot="trigger">Rename project</hui-button>
<h2 slot="title">Rename project</h2>
<p slot="description">Choose a new name. This cannot be undone.</p>
<hui-input name="project" value="Home-UI" aria-label="Project name"></hui-input>
<hui-button slot="footer" variant="ghost" data-dismiss>Cancel</hui-button>
<hui-button slot="footer" data-dismiss>Save</hui-button>
</hui-dialog>

The Go template that renders it:

<hui-dialog>
<hui-button slot="trigger">Rename project</hui-button>
<h2 slot="title">Rename {{ .Project.Name }}</h2>
<p slot="description">Choose a new name. This cannot be undone.</p>
<form hx-post="/projects/{{ .Project.ID }}/rename">
<hui-input name="project" value="{{ .Project.Name }}" aria-label="Project name"></hui-input>
</form>
<hui-button slot="footer" variant="ghost" type="button" data-dismiss>Cancel</hui-button>
<hui-button slot="footer" type="submit" data-dismiss>Save</hui-button>
</hui-dialog>

On a phone

A dialog is never wider than the viewport less 1rem each side, and a form taller than the screen scrolls inside the dialog while its title and footer stay put. A sheet (side) is 85 percent of the width.

A form on a phone 375 px wide
Source
<hui-dialog>
<hui-button slot="trigger" variant="outline">Edit profile</hui-button>
<h2 slot="title">Edit profile</h2>
<p slot="description">Make changes to your profile here. Click save when you’re done.</p>
<div style="display:grid;gap:.75rem">
<hui-field><span data-label>Name</span><hui-input data-control value="Pedro Duarte"></hui-input></hui-field>
<hui-field><span data-label>Username</span><hui-input data-control value="@peduarte"></hui-input></hui-field>
<hui-field><span data-label>Email</span><hui-input data-control type="email" value="pedro@example.com"></hui-input></hui-field>
</div>
<hui-button slot="footer" variant="outline" data-dismiss>Cancel</hui-button>
<hui-button slot="footer" data-dismiss>Save changes</hui-button>
</hui-dialog>

API

Attributes6
Attributes
NameTypeDefaultDescription
openbooleanfalseOpens or closes the dialog through showModal(). Reflected, so an Alpine binding or a server render can drive it.
side"none" | "top" | "right" | "bottom" | "left""none"Pins the panel to an edge as a sheet or drawer. An unknown value falls back to none.
alertbooleanfalseGives the dialog role="alertdialog" and refuses Escape and backdrop dismissal.
dismissiblebooleantrueWhen false, Escape, the backdrop and the close button no longer request hui-dismiss, and the close button is not drawn.
hide-closebooleanfalseLeaves out the close button a dismissible dialog draws at its top right. An alert dialog never has one.
data-dismissboolean, on a slotted control-Not an attribute of the dialog: put it on a control inside it - a footer Cancel - and clicking that control closes the dialog through hide(). Works in an alert dialog too, whose only way out is its own buttons.
Properties5
Properties
NameTypeDefaultDescription
openbooleanfalseReflects to the open attribute.
sideDialogSide'none'Reflects to the side attribute.
alertbooleanfalseReflects to the alert attribute.
dismissiblebooleantrueReflects to the dismissible attribute.
hideClosebooleanfalseReflects to the hide-close attribute.
Methods2
Methods
NameTypeDescription
show(from?)-Opens the dialog and records from as the element focus returns to. The focused element is the fallback when from is omitted.
hide()-Closes the dialog without asking, bypassing hui-dismiss. This is the route an unsaved-changes guard cannot refuse.
Events4
Events
NameDescription
hui-openThe dialog entered the top layer. It bubbles and is composed, and carries no detail.
hui-closeThe dialog left the top layer, after focus has been handed back. It bubbles and is composed, and carries no detail.
hui-dismissA dismissal was requested by Escape, the backdrop or the close button. It bubbles, is composed and is cancelable; calling preventDefault() keeps the dialog open. Never fired while alert or when dismissible is false.
click on [slot="trigger"]Activating the slotted trigger opens the dialog and passes the clicked element as the focus-return target.
Slots5
Slots
NameDescription
triggerThe control that opens the dialog. It stays in the light DOM so the host owns its markup.
titleThe heading, styled through part="title".
descriptionSupporting text under the title, styled through part="description".
(default)The body content, wrapped in part="body".
footerThe action row, wrapped in part="footer" and laid out end-aligned.
CSS custom properties4
CSS custom properties
NameDefaultDescription
--hui-popoveroklch(1 0 0)Panel background.
--hui-popover-foregroundoklch(0.145 0.008 326)Panel text colour.
--hui-radius-xlcalc(var(--hui-radius, 0.45rem) * 1.4)Panel corner radius. A sheet overrides it to 0.
--hui-shadow-lg0 10px 15px -3px oklch(0 0 0 / 0.1), 0 4px 6px -4px oklch(0 0 0 / 0.1)Panel elevation.
::part() hooks8
::part() hooks
NameDescription
triggerThe span wrapping the trigger slot.
dialogThe internal native <dialog>.
headerThe wrapper around the title and description slots.
titleThe title slot wrapper.
descriptionThe description slot wrapper.
bodyThe default slot wrapper.
footerThe footer slot wrapper.
closeThe close button at the top right: nova's ghost icon-sm, named "Close". Last in the panel's order, so opening focuses the first field rather than it.

States

Sheet

side pins the same dialog to an edge and slides it in from there. Everything else is unchanged: the modal behaviour, the focus trap and Escape.

Sheet
Source
<hui-dialog side="right">
<hui-button slot="trigger" variant="outline">Open sheet</hui-button>
<h2 slot="title">Settings</h2>
<p slot="description">A sheet is the same dialog pinned to one edge.</p>
<hui-switch name="notify" aria-label="Notify"></hui-switch>
</hui-dialog>

Alert dialog

alert is the destructive-confirmation form. It has no close button and does not dismiss on a click outside, so the reader has to answer it.

Alert dialog
Source
<hui-dialog alert>
<hui-button slot="trigger" variant="destructive">Delete</hui-button>
<h2 slot="title">Delete this project?</h2>
<p slot="description">Deleting removes every file in it.</p>
<hui-button slot="footer" variant="outline" data-dismiss>Cancel</hui-button>
<hui-button slot="footer" variant="destructive" data-dismiss>Delete</hui-button>
</hui-dialog>

Server-side mechanics

The server owns the whole light-DOM surface: the slot="trigger" control, the title, description, default and footer content, and the side, alert and dismissible attributes. Render <hui-dialog open> and it opens through showModal() as it upgrades, so a state the host persisted returns without a flash, and read open, side, alert and dismissible back because all four reflect. The dialog is not form-associated and submits nothing under any name of its own, though a <form hx-post> inside its body submits exactly as it would anywhere else. An hx-swap that removes an open dialog runs disconnectedCallback(), which closes the native element, so the top layer and the inert page behind are never stranded; a swap that replaces the slotted trigger while it is open means focus falls back to <body> on close, because restoration checks isConnected rather than node identity. Re-rendering it mid-interaction gives whatever state the server echoed: emit open again to keep it up, omit it and the element closes.

Accessibility

  • Built on native <dialog>.showModal(), so the top layer, 50 percent backdrop, inertness of the page behind and Escape are the platform's.
  • The dialog carries role="dialog", or role="alertdialog" when alert is set.
  • Focus returns to the recorded trigger on close; when an hx-swap has removed the trigger, focus falls back to <body> rather than a detached node.
  • hui-dismiss is the cancellable dismissal, so an unsaved-changes guard can keep the dialog up; hide() is the unconditional close.
  • Removing an open dialog from the DOM closes it, so nothing is ever stranded in the top layer with the page still inert.

Keyboard

Keyboard
KeysAction
EscapeRequests dismissal through hui-dismiss. The platform event is always prevented so an alert dialog stays up and a guard can refuse.
Tab, Shift+TabCycles within the modal; the rest of the document is inert while it is open.
Enter, SpaceActivates the focused control in the dialog, including the slotted trigger that opens it.

Gotchas

  • The title and description slots render visually but are never wired to the native <dialog> as aria-labelledby or aria-describedby; a host that needs the dialog announced with a name must set aria-label or aria-labelledby on hui-dialog itself.