Primitives
Alert Dialog
A dialog that stops the user until they answer a question.
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
toast,
} from 'ferry-ui'
import { Trash2 } from 'lucide-react'
export default function AlertDialogHero() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" icon={<Trash2 />}>
Delete project
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
The project “Billing portal” and all its files are deleted. This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive-solid" onClick={() => toast.success('Project deleted')}>
Delete project
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}AlertDialog only for a custom layout.Import the parts. Put them together in this order.
import {
AlertDialog,
AlertDialogAction,
AlertDialogBody,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from 'ferry-ui'
<AlertDialog>
<AlertDialogTrigger />
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle />
<AlertDialogDescription />
</AlertDialogHeader>
<AlertDialogBody />
<AlertDialogFooter>
<AlertDialogCancel />
<AlertDialogAction />
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>| Part | Role |
|---|---|
AlertDialog | Holds the open state. |
AlertDialogTrigger | Opens the dialog on a click. |
AlertDialogContent | The panel. It adds the backdrop. It has no close button. |
AlertDialogHeader | Holds the title and the description. |
AlertDialogTitle | The question. It is the accessible name of the dialog. |
AlertDialogDescription | The consequence of the action. |
AlertDialogBody | Optional content between the header and the footer. |
AlertDialogFooter | The two buttons. Cancel comes first, the action comes last. |
AlertDialogCancel | Closes the dialog and does nothing else. |
AlertDialogAction | Runs onClick, then closes the dialog. |
With asChild, the trigger gives its behavior to its child element. Use it with a Button.
AlertDialogContent renders AlertDialogPortal and AlertDialogOverlay for you. Use these two parts only to build a custom panel.
AlertDialogAction has the primary variant by default. Keep it for an action that deletes nothing. For a destructive action, pass variant="destructive-solid".
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
toast,
} from 'ferry-ui'
import { Send } from 'lucide-react'
export default function AlertDialogPrimaryAction() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button icon={<Send />}>Send invoice</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Send invoice INV-2041?</AlertDialogTitle>
<AlertDialogDescription>
Acme gets the invoice by email. After that, you cannot edit the invoice.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Not yet</AlertDialogCancel>
{/* No variant: the action keeps the default `primary` look. */}
<AlertDialogAction onClick={() => toast.success('Invoice sent')}>Send invoice</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}AlertDialogCancel and AlertDialogAction each render a Button. Each one accepts variant and size. Give the two buttons the same size.
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
} from 'ferry-ui'
export default function AlertDialogButtons() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button>Sign out everywhere</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Sign out of all devices?</AlertDialogTitle>
<AlertDialogDescription>
Each other session of your account ends now. You stay signed in on this device.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel variant="ghost" size="md">
Cancel
</AlertDialogCancel>
<AlertDialogAction variant="warning" size="md">
Sign out everywhere
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Put a list or a warning in AlertDialogBody. If the content is too tall, the body scrolls.
import {
AlertDialog,
AlertDialogAction,
AlertDialogBody,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
} from 'ferry-ui'
const MEMBERS = [
{ name: 'Maya Chen', email: 'maya@example.com', role: 'Admin' },
{ name: 'Sam Lee', email: 'sam@example.com', role: 'Developer' },
{ name: 'Ada Park', email: 'ada@example.com', role: 'Viewer' },
]
export default function AlertDialogWithBody() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Remove 3 members</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Remove 3 members?</AlertDialogTitle>
<AlertDialogDescription>They lose access to each project of this workspace.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogBody className="gap-0 py-2">
{MEMBERS.map((member) => (
<div key={member.email} className="flex items-center justify-between gap-3 border-b py-2 last:border-b-0">
<div className="flex min-w-0 flex-col">
<span className="truncate text-[13px] font-medium text-foreground">{member.name}</span>
<span className="truncate text-xs text-foreground-lighter">{member.email}</span>
</div>
<span className="text-xs text-foreground-light">{member.role}</span>
</div>
))}
</AlertDialogBody>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive-solid">Remove members</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}A click on AlertDialogAction closes the dialog. To keep the dialog open during a request, pass open and onOpenChange. Then call event.preventDefault() in onClick.
Set loading on the action until the request ends. The button shows a spinner and refuses clicks.
import * as React from 'react'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Button,
toast,
} from 'ferry-ui'
export default function AlertDialogAsync() {
const [open, setOpen] = React.useState(false)
const [pending, setPending] = React.useState(false)
function revoke(event: React.MouseEvent) {
// Keep the dialog open until the request ends.
event.preventDefault()
setPending(true)
window.setTimeout(() => {
setPending(false)
setOpen(false)
toast.success('API key revoked')
}, 1200)
}
return (
<AlertDialog
open={open}
onOpenChange={(next) => {
// Escape does not close the dialog while the request runs.
if (!pending) setOpen(next)
}}
>
<AlertDialogTrigger asChild>
<Button variant="destructive">Revoke API key</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Revoke this API key?</AlertDialogTitle>
<AlertDialogDescription>Requests with the key “Analytics export” fail from now on.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive-solid" loading={pending} onClick={revoke}>
Revoke key
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}AlertDialogCancel when the dialog opens. Thus Enter does not confirm the action accidentally.AlertDialogTitle and AlertDialogDescription.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. | ||
| 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. | ||
deferPointerDownOutside | boolean | - |
When | ||
onEscapeKeyDown | ((event: KeyboardEvent) => void) | - |
Event handler called when the escape key is down. Can be prevented. | ||
onFocusOutside | ((event: FocusOutsideEvent) => void) | - |
Event handler called when the focus moves outside of 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. | ||
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||
AlertDialogHeader 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. | ||
AlertDialogBody has no props of its own. It accepts the attributes of the element it renders.
AlertDialogFooter has no props of its own. It accepts the attributes of the element it renders.
| Prop | Type | Default |
|---|---|---|
variant | "link" | "default" | "primary" | "outline" | "ghost" | "destructive" | "destructive-solid" | "danger" | "danger-solid" | "warning" | "dashed" | default |
Look.
| ||
size | "tiny" | "sm" | "md" | "lg" | "icon-tiny" | "icon" | "icon-md" | "icon-lg" | sm |
Height. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
variant | "link" | "default" | "primary" | "outline" | "ghost" | "destructive" | "destructive-solid" | "danger" | "danger-solid" | "warning" | "dashed" | primary |
Look.
| ||
loading | boolean | false |
Shows a spinner before the label, disables the button and sets | ||
size | "tiny" | "sm" | "md" | "lg" | "icon-tiny" | "icon" | "icon-md" | "icon-lg" | sm |
Height. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
children | ReactNode | - |
| See the element or the Radix UI primitive this part renders. | ||
container | Element | DocumentFragment | null | - |
Specify a container element to portal the content into. | ||
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||