Dialog
Use Dialog for a focused task that needs the user's input, such as editing a profile or choosing a plan, without leaving the page. To ask the user to confirm or cancel one action, such as deleting a project, use Alert Dialog instead.
Installation
npx @smicolon/fasla-ui@latest add dialogRun init once per project before adding components. See Installation.
Dialog is built on Base UI, so it needs the @base-ui/react package, and its close and back buttons use button. add installs both with the component, and so does the shadcn CLI.
Preview
Press the button to open the dialog. Focus moves to the first field and Tab stays inside. Escape, the close button, Cancel or a press on the overlay closes it.
Sizes
size on DialogContent sets the width: lg, the default, is 512px, xl is 672px and 2xl is 1148px. Every size is 634px tall, whatever its content. On a smaller screen it keeps 16px from every edge. Choose by content: lg for a short form, 2xl for a table or an editor. Press a button to open that size.
Header
DialogHeader holds the title and an optional description 4px under it, with a line below them. Each stays on one line and ends with an ellipsis when it runs longer, so keep both short. The close button sits at the end of the title's line; showCloseButton on DialogContent turns it off. For a step in a flow, pass onBack to DialogHeader: a back button sits before the title. Its arrow points to the start of the line, so it turns round on an Arabic page.
Actions
Put Buttons in DialogFooter, and wrap Cancel in DialogClose so it closes the dialog. In a row, the default, write Cancel, then the main action: they sit at the end, 8px apart. A third action written before them, such as Skip, goes to the start of the row. layout="stacked" makes each action full width, one under the other, so write the main action first. For an action that can't be undone, give the main Button variant="destructive".
Long content
The dialog is 634px tall whatever its content. When the content is taller, the header and the footer stay where they are and only what sits between them scrolls: scroll inside the dialog below to read on. On a shorter screen the dialog keeps 16px from the top and the bottom and scrolls the same way. A field reached with Tab scrolls into view clear of the header and the footer.
Accessibility
- The dialog has the
dialogrole. Its title names it and its description describes it, so a screen reader reads both when it opens. - When the dialog opens, focus moves to its first control, such as the first field. Tab and Shift+Tab stay inside the dialog, and focus returns to the trigger when it closes.
- Escape, the close button, a
DialogClosebutton and a press on the overlay all close the dialog. - The close button is named Close and the back button Back, or إغلاق and رجوع on an Arabic page. Their icons are hidden from screen readers.
- While the dialog is open, the page behind it can't be reached with the keyboard or the pointer, and it doesn't scroll.
- The dialog fades in as it grows from 98%. When reduced motion is on, it only fades.
- The dialog takes the page's direction, language and font, though it renders at the end of
body. On an Arabic page the whole layout mirrors, and the back arrow turns round. Direction needs no prop.
Props
Dialog
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Shows or hides the dialog, when you control it. |
defaultOpen | boolean | false | Shows the dialog on first render. |
onOpenChange | (open, details) => void | — | Called when the dialog opens or closes. |
modal | boolean | "trap-focus" | true | While the dialog is open, keeps focus inside it and stops the page scrolling. Pass false to let focus leave the dialog and the page scroll, or "trap-focus" to keep focus inside without locking the scroll. |
children | ReactNode | — | The trigger and the content. |
DialogTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | — | Renders your own element in place of the plain button, such as a Button. |
disabled | boolean | false | Stops this trigger opening the dialog. |
children | ReactNode | — | The trigger's label. |
className | string | — | Extra classes for the trigger. |
DialogContent
| Prop | Type | Default | Description |
|---|---|---|---|
size | "lg" | "xl" | "2xl" | "lg" | The width: lg is 512px, xl 672px, 2xl 1148px. |
showCloseButton | boolean | true | Shows the close button at the end of the title's line. |
closeLabel | string | "Close" | The close button's name for screen readers. Defaults to Close, or إغلاق on an Arabic page. |
container | HTMLElement | — | Where the dialog renders. Defaults to body. |
children | ReactNode | — | The header, the content and the footer. |
className | string | — | Extra classes for the dialog box. |
DialogHeader
| Prop | Type | Default | Description |
|---|---|---|---|
onBack | (event) => void | — | Shows a back button before the title, and is called when it is pressed. |
backLabel | string | "Back" | The back button's name for screen readers. Defaults to Back, or رجوع on an Arabic page. |
children | ReactNode | — | The title and the description. |
className | string | — | Extra classes for the header. |
DialogTitle
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | What the dialog is for. |
className | string | — | Extra classes for the title. |
DialogDescription
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A line on what to do in the dialog. |
className | string | — | Extra classes for the description. |
DialogFooter
| Prop | Type | Default | Description |
|---|---|---|---|
layout | "horizontal" | "stacked" | "horizontal" | horizontal puts the actions in a row at the end. stacked makes each one full width, one under the other. |
showCloseButton | boolean | false | Adds an Outline Close button that closes the dialog. |
children | ReactNode | — | The actions: Buttons, with Cancel in a DialogClose. |
className | string | — | Extra classes for the footer. |
DialogClose
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | — | Renders your own element in place of the plain button, such as a Button. |
onClick | (event) => void | — | Runs before the dialog closes. |
disabled | boolean | false | Disables the button. |
children | ReactNode | — | The button's label, such as Cancel. |
className | string | — | Extra classes for the button. |
Usage
Edit a profile
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function EditProfile() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>Edit profile</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Update your name and photo.
</DialogDescription>
</DialogHeader>
<Input label="Email address" type="email" placeholder="Type here" />
<DialogFooter>
<DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Go back a step
"use client"
import * as React from "react"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function NewProject() {
const [step, setStep] = React.useState(1)
return (
<Dialog onOpenChange={(open) => open && setStep(1)}>
<DialogTrigger render={<Button />}>New project</DialogTrigger>
<DialogContent>
{step === 1 ? (
<DialogHeader>
<DialogTitle>Name your project</DialogTitle>
<DialogDescription>Step 1 of 3.</DialogDescription>
</DialogHeader>
) : (
<DialogHeader onBack={() => setStep(1)}>
<DialogTitle>Pick a workspace</DialogTitle>
<DialogDescription>Step 2 of 3.</DialogDescription>
</DialogHeader>
)}
{step === 1 ? (
<Input label="Project name" placeholder="Orbit" />
) : (
<Input label="Workspace name" placeholder="Design team" />
)}
<DialogFooter>
<DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
{step === 1 ? (
<Button onClick={() => setStep(2)}>Next</Button>
) : (
<DialogClose render={<Button />}>Create project</DialogClose>
)}
</DialogFooter>
</DialogContent>
</Dialog>
)
}Close after saving
"use client"
import * as React from "react"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function EditProfileAndSave({ onSave }: { onSave: (email: string) => Promise<void> }) {
const [open, setOpen] = React.useState(false)
const [pending, setPending] = React.useState(false)
const [email, setEmail] = React.useState("")
return (
<Dialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
<DialogTrigger render={<Button variant="outline" />}>Edit profile</DialogTrigger>
<DialogContent>
<form
className="contents"
onSubmit={async (event) => {
event.preventDefault()
setPending(true)
try {
await onSave(email)
setOpen(false)
} finally {
setPending(false)
}
}}
>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
</DialogHeader>
<Input
label="Email address"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<DialogFooter>
<DialogClose disabled={pending} render={<Button variant="outline" />}>
Cancel
</DialogClose>
<Button type="submit" loading={pending} loadingLabel="Saving">
Save changes
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}