--- title: Dialog description: A task done in a panel over the page, with its buttons in the band beneath it. --- ```tsx lineNumbers 'use client'; import { Button } from '@/components/panelui/button'; import { Dialog } from '@/components/panelui/dialog'; import { Input } from '@/components/panelui/input'; import { Label } from '@/components/panelui/label'; import { useState } from 'react'; export default function DialogOverview() { const [open, setOpen] = useState(false); const [name, setName] = useState('Spring launch'); return ( }>Rename board
{ event.preventDefault(); const next = new FormData(event.currentTarget).get('name'); if (typeof next === 'string' && next.trim()) setName(next.trim()); setOpen(false); }} >
}>Cancel
); } ```
Use it for a short task that is done away from the page and then put away: renaming something, inviting people, filling in a few fields. The task sits in a panel, and `Dialog.Footer` moves out of the panel into the darker band underneath, where its buttons are equal pills. `variant="plain"` is a single surface with the footer ruled off at its foot, for a dense form. A press outside, Escape or the close button in the corner dismiss it. The dialog is never taller than the screen: the header and footer stay put and `Dialog.Body` scrolls between them. On a screen under 640px wide it rests at the bottom, where a thumb reaches its buttons. For a yes-or-no question, use [Alert Dialog](/docs/web/components/alert-dialog). For a longer task that keeps the page in view, use [Sheet](/docs/web/components/sheet). ## Installation The source is copied into `components/panelui/`, where you can edit it. Import from `panelui-web/dialog` instead of `@/components/panelui/dialog`. Tailwind needs one line to read the package's classes, which is on the [installation page](/docs/web/installation#from-npm). Install the packages it uses: Copy the source into `components/panelui/dialog.tsx`: ## Usage ```tsx import { Dialog } from "@/components/panelui/dialog" ``` ```tsx }>Rename board }>Cancel ``` ## Composition ```tsx ``` - **`Dialog.Trigger`** — What opens the dialog. Pass your own button as `render`. - **`Dialog.Content`** — The dialog, over a dimmed page. `variant` is `inset` (a panel in a band) or `plain` (one surface), `size` is `sm`, `md` or `lg`, `showClose` draws the round close button, and `blur` frosts the page behind it. - **`Dialog.Header`** — The title and the line under it, given as `title` and `description` or as parts. It leaves room for the close button. Anything else in it, such as a search field, stays put while the body scrolls. - **`Dialog.Title`** — The dialog's name. It is announced when the dialog opens. - **`Dialog.Description`** — A sentence under the title, read out after it. - **`Dialog.Body`** — The part that scrolls, with a thin drawn thumb. Its children are laid out in a column with a gap, so the fields of a form need no wrapper. A hairline appears under the header once its content has scrolled. - **`Dialog.List`** — A list of one-line choices, such as time zones or the projects to move something to. The arrow keys, Home and End move between its items, and it opens scrolled to the selected one. - **`Dialog.ListGroup`** — A run of items under a `label`, which stays pinned at the top while its items scroll under it. - **`Dialog.ListItem`** — One choice. `selected` tints and checks it, `onSelect` is called when it is pressed or chosen with Enter, and `meta` puts quieter text at its end in figures that line up. - **`Dialog.Footer`** — The buttons. In the inset look they sit in the band as equal pills; in `plain` they sit at the end of a ruled strip. Keep it a direct child of `Dialog.Content`. - **`Dialog.Close`** — Closes the dialog. Pass your own button as `render`, such as Cancel. ## Examples ### Inviting people A field, a role [Select](/docs/web/components/select) and an Add button in one row, over the list of people invited so far. Pressing Enter in the field adds the address. ```tsx lineNumbers 'use client'; import { Avatar } from '@/components/panelui/avatar'; import { Button } from '@/components/panelui/button'; import { Dialog } from '@/components/panelui/dialog'; import { Frame } from '@/components/panelui/frame'; import { Input } from '@/components/panelui/input'; import { Select } from '@/components/panelui/select'; import { useState } from 'react'; const ROLES = [ { value: 'editor', label: 'Editor' }, { value: 'viewer', label: 'Viewer' }, ]; type Invite = { email: string; role: string }; export default function DialogInvite() { const [invites, setInvites] = useState([ { email: 'ines@quarry.dev', role: 'editor' }, { email: 'tomas@quarry.dev', role: 'viewer' }, ]); const [email, setEmail] = useState(''); const [role, setRole] = useState('editor'); const add = () => { const next = email.trim(); if (!next.includes('@') || invites.some((invite) => invite.email === next)) return; setInvites([...invites, { email: next, role }]); setEmail(''); }; return ( }>Invite people
{ event.preventDefault(); add(); }} > setEmail(event.target.value)} className="min-w-0 flex-1" />
{invites.map((invite) => ( {invite.email} {invite.role === 'editor' ? 'Editor' : 'Viewer'} ))}
}>Cancel }> Send {invites.length} {invites.length === 1 ? 'invite' : 'invites'}
); } ```
### A form A name, an address with a fixed prefix in an `Input.Group`, a [Textarea](/docs/web/components/textarea) and a [Select](/docs/web/components/select). The form lives in the body and the submit button in the footer, joined by the form's `id`. An empty name marks its [Label](/docs/web/components/label) invalid; Create shows a spinner while it saves. `dismissible={false}` keeps a stray press outside from throwing the form away. ```tsx lineNumbers 'use client'; import { Button } from '@/components/panelui/button'; import { Dialog } from '@/components/panelui/dialog'; import { Input } from '@/components/panelui/input'; import { Label } from '@/components/panelui/label'; import { Select } from '@/components/panelui/select'; import { Textarea } from '@/components/panelui/textarea'; import { useState } from 'react'; const VISIBILITY = [ { value: 'team', label: 'Everyone in the team' }, { value: 'private', label: 'Only people I invite' }, ]; export default function DialogForm() { const [open, setOpen] = useState(false); const [saving, setSaving] = useState(false); const [missing, setMissing] = useState(false); return ( { setOpen(next); if (!next) setMissing(false); }} dismissible={false} > }>New project
{ event.preventDefault(); const name = String(new FormData(event.currentTarget).get('name') ?? '').trim(); setMissing(name === ''); if (name === '') return; setSaving(true); window.setTimeout(() => { setSaving(false); setOpen(false); }, 1100); }} >
quarry.app/