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.
Preview
Section titled “Preview”<nana-dialog id="dialog-preview" label="Delete project">
<nana-dialog-header>Delete project?</nana-dialog-header>
<nana-dialog-body>
The project and its history are removed for everyone. This cannot be undone.
</nana-dialog-body>
<nana-dialog-footer>
<nana-button variant="ghost" onclick="this.closest('nana-dialog').close()">
Cancel
</nana-button>
<nana-button variant="danger" onclick="this.closest('nana-dialog').close()">
Delete
</nana-button>
</nana-dialog-footer>
</nana-dialog>
<nana-button onclick="document.getElementById('dialog-preview').show()">
Open dialog
</nana-button> Installation
Section titled “Installation”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 dialogLands as src/components/ui/dialog/dialog.tsx, styled with Tailwind classes that
resolve to the design tokens. Requires Tailwind v4.
npx @nana-tec/ui add dialogLands in src/lib/components/ui/dialog/ as Dialog.svelte, DialogHeader.svelte, DialogTitle.svelte, DialogBody.svelte and DialogFooter.svelte, styled with Tailwind
classes that resolve to the design tokens. Requires Tailwind v4.
Install the package for a versioned dependency:
npm install @nana-tec/ui-componentsOr copy the source in and own it:
npx @nana-tec/ui add dialogimport { 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}.
<script lang="ts"> import { Dialog, DialogHeader, DialogTitle, DialogBody, DialogFooter, } from '$lib/components/ui/dialog'; import { Button } from '$lib/components/ui/button';
let open = $state(false);</script>
<Button onclick={() => (open = true)}>Open</Button>
<Dialog bind:open size="md" label="Confirm"> <DialogHeader> <DialogTitle>Delete project</DialogTitle> </DialogHeader> <DialogBody>This cannot be undone.</DialogBody> <DialogFooter> <Button variant="ghost" onclick={() => (open = false)}>Cancel</Button> <Button variant="danger">Delete</Button> </DialogFooter></Dialog>open is $bindable, so bind:open closes it for you; an optional onclose
fires alongside. 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}.
import "@nana-tec/ui-components/dialog";<nana-dialog id="delete-dialog" label="Delete project"> <nana-dialog-header>Delete project?</nana-dialog-header> <nana-dialog-body>This cannot be undone.</nana-dialog-body> <nana-dialog-footer> <nana-button variant="ghost" onclick="this.closest('nana-dialog').close()">Cancel</nana-button> <nana-button variant="danger">Delete</nana-button> </nana-dialog-footer></nana-dialog>
<nana-button onclick="document.getElementById('delete-dialog').show()">Open dialog</nana-button>Classes you pass to the React and Svelte components are merged with
tailwind-merge, so an override wins rather than fighting. See
Using Tailwind.
Subcomponents
Section titled “Subcomponents”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>Variants & sizes
Section titled “Variants & sizes”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>Close button
Section titled “Close button”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.
<nana-dialog id="dc" close-button>
<nana-dialog-header>Receipt RCT-1041</nana-dialog-header>
<nana-dialog-body>KSh 12,800 cleared on 14 March.</nana-dialog-body>
</nana-dialog>
<nana-button onclick="dc.show()">Open</nana-button> 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
| Prop | Type | Default |
|---|---|---|
openrequired | boolean | |
onCloserequired | () => void | |
size | DialogSize | 'md' |
label | string | |
dismissible | boolean | true |
children | ReactNode |
Also accepts Styled<DialogPart>Omit<HTMLAttributes<HTMLDivElement>, 'children'>.
DialogHeader
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
Also accepts HTMLAttributes<HTMLDivElement>.
DialogTitle
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
Also accepts HTMLAttributes<HTMLHeadingElement>.
DialogBody
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
Also accepts HTMLAttributes<HTMLDivElement>.
DialogFooter
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
Also accepts HTMLAttributes<HTMLDivElement>.
Dialog
| Prop | Type | Default |
|---|---|---|
openbind: | boolean | false |
size | DialogSize | 'md' |
label | string | |
dismissible | boolean | true |
onclose | () => void | |
children | Snippet |
The attribute names, events and CSS hooks below are the Web Components reference.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
ariaLabel | aria-label | string | null | null | Accessible label (use when there is no visible heading) |
closeButton | close-button | boolean | false | Show a close button in the panel's top corner. |
closeLabel | close-label | string | 'Close' | Names the close button for assistive tech. |
noBackdropClose | no-backdrop-close | boolean | false | Don't close when the backdrop is clicked |
noEscapeClose | no-escape-close | boolean | false | Don't close when Escape is pressed |
open | open | boolean | false | Whether the dialog is open |
placement | placement | DialogPlacement | 'end' | Which edge a drawer slides from |
size | size | DialogSize | 'md' | Panel size |
variant | variant | DialogVariant | 'modal' | Behaviour / placement style |
Events
| Event | Description |
|---|---|
nana-open | Fired after the dialog opens |
nana-close | Fired after the dialog closes (detail.returnValue) |
Slots
| Slot | Description |
|---|---|
(default) | Dialog content (header / body / footer) |
CSS parts
| Part | Description |
|---|---|
::part(dialog) | The native <dialog> element |
::part(panel) | The content surface |
::part(close-button) | The built-in close button |
CSS custom properties
| Property | Description |
|---|---|
--nana-dialog-width | Panel width (per size by default) |
--nana-dialog-backdrop | Backdrop colour |
--nana-dialog-radius | Panel corner radius |
--nana-dialog-bg | Panel background |
--nana-dialog-sheet-duration | How long the dialog takes to rise as a bottom sheet on phones |
Events
Section titled “Events”| Event | Type | Description |
|---|---|---|
nana-open | CustomEvent | Fired after the dialog opens |
nana-close | CustomEvent | Fired after the dialog closes (detail.returnValue) |
| Slot | Description |
|---|---|
(default) | Dialog content (header / body / footer) |
CSS Variables
Section titled “CSS Variables”| Variable | Description |
|---|---|
--nana-dialog-width | Panel width (per size by default) |
--nana-dialog-backdrop | Backdrop colour |
--nana-dialog-radius | Panel corner radius |
CSS Parts
Section titled “CSS Parts”| Part | Description |
|---|---|
dialog | The native <dialog> element |
panel | The content surface |
close-button | The built-in close button |
Accessibility
Section titled “Accessibility”- Keyboard accessible
- Screen-reader labels
- Focus-visible states
- ARIA roles and states
- WCAG 2.2 AA contrast