Skip to content
Nana UI

Layout & Containers

Dialog

A modal dialog built on the native <dialog> element — so it gets the top layer, a backdrop, focus trapping and Escape-to-close for free. Compose it from nana-dialog-header, nana-dialog-body and nana-dialog-footer.

View .md Download
Delete project? The project and its history are removed for everyone. This cannot be undone. Cancel Delete Open dialog

The dialog ships for all three targets. Pick the tab for your project — the CLI detects which you are on and serves the matching implementation.

npx @nana-tec/ui add dialog

Lands as src/components/ui/dialog/dialog.tsx, styled with Tailwind classes that resolve to the design tokens. Requires Tailwind v4.

import {
Dialog, DialogHeader, DialogTitle, DialogBody, DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export function Example() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Open</Button>
<Dialog open={open} onClose={() => setOpen(false)} size="md" label="Confirm">
<DialogHeader>
<DialogTitle>Delete project</DialogTitle>
</DialogHeader>
<DialogBody>This cannot be undone.</DialogBody>
<DialogFooter>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="danger">Delete</Button>
</DialogFooter>
</Dialog>
</>
);
}

It is fully controlled: open and onClose are both required. The dialog traps focus while open, restores it on close, locks body scroll, and closes on Escape or a backdrop click unless you set dismissible={false}.

Classes you pass to the React and Svelte components are merged with tailwind-merge, so an override wins rather than fighting. See Using Tailwind.

Compose the dialog with nana-dialog-header, nana-dialog-body, and nana-dialog-footer. Open it with the open attribute, .show(), or a data-open="#id" trigger; close with data-close.

<nana-button data-open="#invite">Invite teammate</nana-button>
<nana-dialog id="invite" aria-label="Invite teammate">
<nana-dialog-header>Invite teammate</nana-dialog-header>
<nana-dialog-body>
<nana-input label="Email" type="email" placeholder="ada@example.com"></nana-input>
</nana-dialog-body>
<nana-dialog-footer>
<nana-button variant="ghost" data-close>Cancel</nana-button>
<nana-button variant="primary" data-close>Send invite</nana-button>
</nana-dialog-footer>
</nana-dialog>

Set variant (modal, alert, drawer, fullscreen) and size (xs–xl, fullscreen). Use placement="start|end" for the drawer variant.

<nana-dialog variant="alert" size="sm" aria-label="Delete project">…</nana-dialog>
<nana-dialog variant="drawer" placement="end" aria-label="Settings">…</nana-dialog>
<nana-dialog variant="fullscreen" aria-label="Editor">…</nana-dialog>

Escape and a backdrop click both close a dialog, but neither is visible, and on a touch screen there is no Escape key at all. close-button puts a labelled close control in the panel’s top corner — the same attribute, part and glyph nana-drawer uses, so the two overlays behave alike.

Receipt RCT-1041 KSh 12,800 cleared on 14 March. Open

close-label renames it for assistive tech; it reads “Close” otherwise. Style it through ::part(close-button).

The React and Svelte props, read from the components’ own types:

Dialog

PropTypeDefault
openrequiredboolean
onCloserequired() => void
sizeDialogSize'md'
labelstring
dismissiblebooleantrue
childrenReactNode

Also accepts Styled<DialogPart>Omit<HTMLAttributes<HTMLDivElement>, 'children'>.

DialogHeader

PropTypeDefault
asChildbooleanfalse

Also accepts HTMLAttributes<HTMLDivElement>.

DialogTitle

PropTypeDefault
asChildbooleanfalse

Also accepts HTMLAttributes<HTMLHeadingElement>.

DialogBody

PropTypeDefault
asChildbooleanfalse

Also accepts HTMLAttributes<HTMLDivElement>.

DialogFooter

PropTypeDefault
asChildbooleanfalse

Also accepts HTMLAttributes<HTMLDivElement>.

The attribute names, events and CSS hooks below are the Web Components reference.

Properties

PropertyAttributeTypeDefaultDescription
ariaLabelaria-labelstring | nullnullAccessible label (use when there is no visible heading)
closeButtonclose-buttonbooleanfalseShow a close button in the panel's top corner.
closeLabelclose-labelstring'Close'Names the close button for assistive tech.
noBackdropCloseno-backdrop-closebooleanfalseDon't close when the backdrop is clicked
noEscapeCloseno-escape-closebooleanfalseDon't close when Escape is pressed
openopenbooleanfalseWhether the dialog is open
placementplacementDialogPlacement'end'Which edge a drawer slides from
sizesizeDialogSize'md'Panel size
variantvariantDialogVariant'modal'Behaviour / placement style

Events

EventDescription
nana-openFired after the dialog opens
nana-closeFired after the dialog closes (detail.returnValue)

Slots

SlotDescription
(default)Dialog content (header / body / footer)

CSS parts

PartDescription
::part(dialog)The native <dialog> element
::part(panel)The content surface
::part(close-button)The built-in close button

CSS custom properties

PropertyDescription
--nana-dialog-widthPanel width (per size by default)
--nana-dialog-backdropBackdrop colour
--nana-dialog-radiusPanel corner radius
--nana-dialog-bgPanel background
--nana-dialog-sheet-durationHow long the dialog takes to rise as a bottom sheet on phones
EventTypeDescription
nana-openCustomEventFired after the dialog opens
nana-closeCustomEventFired after the dialog closes (detail.returnValue)
SlotDescription
(default)Dialog content (header / body / footer)
VariableDescription
--nana-dialog-widthPanel width (per size by default)
--nana-dialog-backdropBackdrop colour
--nana-dialog-radiusPanel corner radius
PartDescription
dialogThe native <dialog> element
panelThe content surface
close-buttonThe built-in close button
  • Keyboard accessible
  • Screen-reader labels
  • Focus-visible states
  • ARIA roles and states
  • WCAG 2.2 AA contrast
Explore Dialog interactively Every state, prop, and edge case — with live controls — in Storybook.
View in Storybook