Dialog
A task the page waits for. The dialog is one frame over a dimmed page: a header, a body that scrolls when it runs long, and its actions as cells sharing the bottom keyline. Focus moves in, stays in, and returns to the trigger; Escape and the close cell put it away.
Loading the live example…
"use client"
import { useState } from "react"
import { Button } from "@/components/keyline/ui/button"
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/keyline/ui/dialog"
import { Field, FieldDescription, FieldError, FieldFrame, FieldInput, FieldLabel, Form } from "@/components/keyline/ui/field"
export function NewClient() {
const [open, setOpen] = useState(false)
const [clients, setClients] = useState(["Lackawanna County", "Blue Cross of Pennsylvania"])
return (
<div className="grid gap-4">
<ul aria-label="Clients" className="m-0 grid list-none gap-2 p-0 text-[14px]">
{clients.map((c) => (
<li key={c}>{c}</li>
))}
</ul>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger render={<Button className="justify-self-start" />}>Add a client</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add a client</DialogTitle>
<DialogDescription>They join Dwight's accounts until you assign a rep.</DialogDescription>
</DialogHeader>
<DialogBody>
{/* the footer's submit button names this form, so the form can stay in the scrolling body */}
<Form
id="new-client"
onFormSubmit={(v) => {
setClients((all) => [...all, String(v.company).trim()])
setOpen(false)
}}
>
<Field name="company">
<FieldLabel>Company</FieldLabel>
<FieldFrame>
<FieldInput required autoComplete="organization" />
</FieldFrame>
<FieldError match="valueMissing">Which company?</FieldError>
</Field>
<Field name="contact">
<FieldLabel>Contact</FieldLabel>
<FieldFrame>
<FieldInput autoComplete="name" />
</FieldFrame>
<FieldDescription>Who orders the paper.</FieldDescription>
</Field>
</Form>
</DialogBody>
<DialogFooter>
<DialogClose render={<Button layout="cell" />}>Cancel</DialogClose>
<Button layout="cell" variant="primary" type="submit" form="new-client">
Add client
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}Installation
npx shadcn@latest add @keyline-ui/dialogRun it where your components.json is. The first time, map the @keyline-ui registry: one command, in Installation.
Usage
import { Dialog, DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/keyline/ui/dialog"<Dialog>
<DialogTrigger render={<Button />}>Add a client</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add a client</DialogTitle>
<DialogDescription>…</DialogDescription>
</DialogHeader>
<DialogBody>…</DialogBody>
<DialogFooter>
<DialogClose render={<Button layout="cell" />}>Cancel</DialogClose>
<Button layout="cell" variant="primary" type="submit" form="new-client">Add client</Button>
</DialogFooter>
</DialogContent>
</Dialog>Props
| Prop | Type | Default | Description |
|---|---|---|---|
open · defaultOpen | boolean | Default — | On Dialog: control it with onOpenChange, for example to close after a form submits. |
render | ReactElement | Default — | On DialogTrigger and DialogClose: the element to render, usually a Button. |
container | HTMLElement | ref | Default document.body | On DialogContent: where the dialog is portalled. |
DialogBody | div | Default — | Scrolls in a Keyline Scroll Area once the dialog reaches its maximum height. The header and footer stay put. |
DialogFooter | div | Default — | A row of cells. Use Button layout="cell"; a submit button can name a form in the body with form. |
Built on Base UI Dialog; its other props pass through. To confirm something that can't be undone, use Alert dialog.