Primitives
Dialog
A window on top of the page for one task or one form.
import {
Button,
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
Field,
Input,
} from 'ferry-ui'
export default function DialogHero() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Rename project</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Rename project</DialogTitle>
<DialogDescription>The new name shows in the list of projects.</DialogDescription>
</DialogHeader>
<DialogBody>
<Field label="Name">
<Input defaultValue="Billing portal" />
</Field>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button>Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="primary">Save</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}DialogTitle is the accessible name of the dialog.Import the parts and put them together.
import {
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from 'ferry-ui'
<Dialog>
<DialogTrigger />
<DialogContent>
<DialogHeader>
<DialogTitle />
<DialogDescription />
</DialogHeader>
<DialogBody />
<DialogFooter>
<DialogClose />
</DialogFooter>
</DialogContent>
</Dialog>| Part | Role |
|---|---|
Dialog | Holds the open state. |
DialogTrigger | Opens the dialog on a click. |
DialogContent | The panel. It adds the overlay and the close button. |
DialogHeader | Holds the title and the description. |
DialogBody | The content. It is the only part that scrolls. |
DialogFooter | The actions. Cancel comes first, the primary action comes last. |
DialogClose | Closes the dialog on a click. |
A dialog holds its open state by default. Use defaultOpen to open it at the start.
To control the state, pass open and onOpenChange. Use this to close the dialog after a submit.
import * as React from 'react'
import {
Button,
Dialog,
DialogBody,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Field,
Input,
toast,
} from 'ferry-ui'
export default function DialogControlled() {
const [open, setOpen] = React.useState(false)
function invite(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
toast.success('Invitation sent')
// The form is done: close the dialog from the code.
setOpen(false)
}
return (
<>
<Button onClick={() => setOpen(true)}>Invite a member</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<form onSubmit={invite}>
<DialogHeader>
<DialogTitle>Invite a member</DialogTitle>
<DialogDescription>The member gets an email with a link.</DialogDescription>
</DialogHeader>
<DialogBody>
<Field label="Email">
<Input type="email" required placeholder="maya@example.com" />
</Field>
</DialogBody>
<DialogFooter>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button type="submit" variant="primary">
Send invitation
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
)
}The size prop of DialogContent sets the maximum width.
import {
Button,
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from 'ferry-ui'
const SIZES = ['sm', 'md', 'lg', 'xl', 'xxl'] as const
export default function DialogSizes() {
return (
<>
{SIZES.map((size) => (
<Dialog key={size}>
<DialogTrigger asChild>
<Button>{size}</Button>
</DialogTrigger>
<DialogContent size={size}>
<DialogHeader>
<DialogTitle>Size {size}</DialogTitle>
<DialogDescription>The panel takes this width on a screen that is wide enough.</DialogDescription>
</DialogHeader>
<DialogBody>
<p className="text-[13px] text-foreground-light">On a phone, the panel takes the width of the screen.</p>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="primary">Done</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
))}
</>
)
}| Size | Width |
|---|---|
sm | 384px |
md | 448px (default) |
lg | 512px |
xl | 672px |
xxl | 896px |
Put long content in DialogBody. The body scrolls. The header and the footer stay in view.
import {
Button,
Dialog,
DialogBody,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from 'ferry-ui'
const CHANGES = [
'Invoices show the tax for each line.',
'Members can have more than one role.',
'API keys can expire on a date.',
'The audit log keeps 90 days of events.',
'Projects can move to another workspace.',
'Exports include the archived orders.',
'The search finds customers by email.',
'Webhooks send a new event for refunds.',
'The dashboard shows the usage for each day.',
'Notifications have a weekly summary.',
'The billing page lists the past payments.',
'Each order shows its delivery status.',
]
export default function DialogScroll() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Release notes</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Release notes</DialogTitle>
<DialogDescription>The changes of this month.</DialogDescription>
</DialogHeader>
<DialogBody className="max-h-64">
<ul className="flex flex-col gap-3 text-[13px] text-foreground-light">
{CHANGES.map((change) => (
<li key={change}>{change}</li>
))}
</ul>
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="primary">Done</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}A dialog with no DialogDescription must tell that to screen readers. Pass aria-describedby={undefined} to DialogContent.
import { Button, Dialog, DialogBody, DialogContent, DialogHeader, DialogTitle, DialogTrigger, Kbd, useModKey } from 'ferry-ui'
export default function DialogNoDescription() {
const mod = useModKey()
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost">Keyboard shortcuts</Button>
</DialogTrigger>
<DialogContent size="sm" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>Keyboard shortcuts</DialogTitle>
</DialogHeader>
<DialogBody>
<p className="flex items-center justify-between text-[13px] text-foreground-light">
Open the command menu <Kbd>{mod} K</Kbd>
</p>
<p className="flex items-center justify-between text-[13px] text-foreground-light">
Close a dialog <Kbd>Esc</Kbd>
</p>
</DialogBody>
</DialogContent>
</Dialog>
)
}closeLabel sets the accessible name of the close button. The default is "Close".Each part also accepts the props of its Radix UI primitive and the attributes of its element.
| Prop | Type | Default |
|---|---|---|
children | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
open | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
defaultOpen | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onOpenChange | ((open: boolean) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
modal | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "xxl" | md |
Max width from the | ||
showCloseButton | boolean | true |
Renders the top-right close (X) button. Default | ||
closeLabel | string | Close |
Accessible name of the built-in close (X) button. Default | ||
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
deferPointerDownOutside | boolean | - |
When | ||
onEscapeKeyDown | ((event: KeyboardEvent) => void) | - |
Event handler called when the escape key is down. Can be prevented. | ||
onPointerDownOutside | ((event: PointerDownOutsideEvent) => void) | - |
Event handler called when the a | ||
onFocusOutside | ((event: FocusOutsideEvent) => void) | - |
Event handler called when the focus moves outside of the | ||
onInteractOutside | ((event: FocusOutsideEvent | PointerDownOutsideEvent) => void) | - |
Event handler called when an interaction happens outside the | ||
onOpenAutoFocus | ((event: Event) => void) | - |
Event handler called when auto-focusing on open. Can be prevented. | ||
onCloseAutoFocus | ((event: Event) => void) | - |
Event handler called when auto-focusing on close. Can be prevented. | ||
DialogHeader has no props of its own. It accepts the attributes of the element it renders.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
DialogBody has no props of its own. It accepts the attributes of the element it renders.
DialogFooter has no props of its own. It accepts the attributes of the element it renders.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||