Dialog
A window over the page — centred, a bottom sheet or a side panel — and AlertDialog for a confirmation.
Web uniquement — React, depuis @krizaka/ui/dialog. Stable : son API ne change qu’à une version majeure.
Quand l’utiliser
- For a short task that must be finished or cancelled before going back: invite someone, edit a title.
AlertDialogto confirm a destructive or irreversible action, with an asynconConfirm.Sheet(bottom) on phones,placement="right"for a side panel of details.- A gate the user must answer (age, terms):
hideCloseanddismissible={false}.
Quand ne pas l’utiliser
- For information next to an element, without blocking the page.À la place : Popover
- For a message that needs no answer.À la place : Toast
- For a long form or a flow of several steps: give it its own page.
- To confirm the deletion of one item in place.À la place : Confirm button
Installation
Installer
npm install @krizaka/ui@beta @krizaka/tailwind@beta @krizaka/tokens@beta tailwindcssStyles
@import "tailwindcss";
@import "@krizaka/tailwind";
@import "@krizaka/ui/tailwind.css";Importer
import { Dialog, Sheet, AlertDialog } from "@krizaka/ui/dialog";Exemples
Centred
A short form, size="md".
import { Button } from "@krizaka/ui/button";
import { Dialog } from "@krizaka/ui/dialog";
import { Field, Input } from "@krizaka/ui/field";
export default function DialogCenter({ defaultOpen = false }: { defaultOpen?: boolean }) {
return (
<Dialog.Root defaultOpen={defaultOpen}>
<Dialog.Trigger asChild>
<Button>Open</Button>
</Dialog.Trigger>
<Dialog.Content closeLabel="Close">
<Dialog.Header>
<Dialog.Title>Invite a collaborator</Dialog.Title>
<Dialog.Description>They get access to the drafts of this channel.</Dialog.Description>
</Dialog.Header>
<Dialog.Body>
<Field.Root>
<Field.Label htmlFor="invite-email">Email</Field.Label>
<Input id="invite-email" type="email" placeholder="name@example.com" />
</Field.Root>
</Dialog.Body>
<Dialog.Footer>
<Dialog.Close asChild>
<Button variant="ghost">Cancel</Button>
</Dialog.Close>
<Button variant="primary">Send the invitation</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
);
}Bottom sheet
placement="bottom": a sheet on a phone, centred from sm up.
import { Button } from "@krizaka/ui/button";
import { Dialog } from "@krizaka/ui/dialog";
import { Field, Input } from "@krizaka/ui/field";
// `placement="bottom"`: a sheet on a phone, centred from `sm` up.
export default function DialogBottom({ defaultOpen = false }: { defaultOpen?: boolean }) {
return (
<Dialog.Root defaultOpen={defaultOpen}>
<Dialog.Trigger asChild>
<Button>Open</Button>
</Dialog.Trigger>
<Dialog.Content closeLabel="Close" placement="bottom">
<Dialog.Header>
<Dialog.Title>Invite a collaborator</Dialog.Title>
<Dialog.Description>They get access to the drafts of this channel.</Dialog.Description>
</Dialog.Header>
<Dialog.Body>
<Field.Root>
<Field.Label htmlFor="invite-email-bottom">Email</Field.Label>
<Input id="invite-email-bottom" type="email" placeholder="name@example.com" />
</Field.Root>
</Dialog.Body>
<Dialog.Footer>
<Dialog.Close asChild>
<Button variant="ghost">Cancel</Button>
</Dialog.Close>
<Button variant="primary">Send the invitation</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
);
}Side panel
placement="right": full height, for details.
import { Button } from "@krizaka/ui/button";
import { Dialog } from "@krizaka/ui/dialog";
import { Field, Input } from "@krizaka/ui/field";
// `placement="right"`: a side panel, full height.
export default function DialogRight({ defaultOpen = false }: { defaultOpen?: boolean }) {
return (
<Dialog.Root defaultOpen={defaultOpen}>
<Dialog.Trigger asChild>
<Button>Open</Button>
</Dialog.Trigger>
<Dialog.Content closeLabel="Close" placement="right">
<Dialog.Header>
<Dialog.Title>Invite a collaborator</Dialog.Title>
<Dialog.Description>They get access to the drafts of this channel.</Dialog.Description>
</Dialog.Header>
<Dialog.Body>
<Field.Root>
<Field.Label htmlFor="invite-email-right">Email</Field.Label>
<Input id="invite-email-right" type="email" placeholder="name@example.com" />
</Field.Root>
</Dialog.Body>
<Dialog.Footer>
<Dialog.Close asChild>
<Button variant="ghost">Cancel</Button>
</Dialog.Close>
<Button variant="primary">Send the invitation</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
);
}Sheet with a form
Sheet, size="lg".
import { Button } from "@krizaka/ui/button";
import { Dialog, Sheet } from "@krizaka/ui/dialog";
import { Field, Input, Textarea } from "@krizaka/ui/field";
// `Sheet` = `Dialog.Content placement="bottom"`: a form, `size="lg"`.
export default function DialogSheetForm({ defaultOpen = false }: { defaultOpen?: boolean }) {
return (
<Dialog.Root defaultOpen={defaultOpen}>
<Dialog.Trigger asChild>
<Button>Edit</Button>
</Dialog.Trigger>
<Sheet closeLabel="Close" size="lg">
<Dialog.Header>
<Dialog.Title>Edit the video</Dialog.Title>
<Dialog.Description>The title and the description are public.</Dialog.Description>
</Dialog.Header>
<Dialog.Body className="flex flex-col gap-4">
<Field.Root>
<Field.Label htmlFor="video-title">Title</Field.Label>
<Input id="video-title" defaultValue="Night ride across the city" />
</Field.Root>
<Field.Root>
<Field.Label htmlFor="video-description">Description</Field.Label>
<Textarea id="video-description" defaultValue="Ten minutes of neon, rain and empty avenues." />
</Field.Root>
</Dialog.Body>
<Dialog.Footer>
<Dialog.Close asChild>
<Button variant="ghost">Cancel</Button>
</Dialog.Close>
<Button variant="primary">Save</Button>
</Dialog.Footer>
</Sheet>
</Dialog.Root>
);
}Alert dialog
AlertDialog tone="danger" with an async onConfirm.
"use client";
import { Button } from "@krizaka/ui/button";
import { AlertDialog } from "@krizaka/ui/dialog";
// An async `onConfirm`: the button loads until it settles; the dialog closes when it resolves, stays open when it rejects.
export default function DialogAlertDestructive({ defaultOpen = false }: { defaultOpen?: boolean }) {
return (
<AlertDialog
defaultOpen={defaultOpen}
trigger={<Button variant="danger">Delete the video</Button>}
title="Delete this video?"
description="Its views, comments and earnings history go with it. This cannot be undone."
confirmLabel="Delete"
cancelLabel="Keep it"
tone="danger"
onConfirm={() => new Promise((resolve) => setTimeout(resolve, 1200))}
/>
);
}Gate
No close button: it closes only through its actions.
import { Button } from "@krizaka/ui/button";
import { Dialog } from "@krizaka/ui/dialog";
// A question the user must answer: no close button, Escape and the overlay do nothing — it closes through its actions.
export default function DialogGate({ defaultOpen = false }: { defaultOpen?: boolean }) {
return (
<Dialog.Root defaultOpen={defaultOpen}>
<Dialog.Trigger asChild>
<Button>Enter the channel</Button>
</Dialog.Trigger>
<Dialog.Content hideClose dismissible={false} size="sm">
<Dialog.Header className="pr-5">
<Dialog.Title>Are you 18 or older?</Dialog.Title>
<Dialog.Description>This channel shows content for adults only.</Dialog.Description>
</Dialog.Header>
<Dialog.Footer>
<Dialog.Close asChild>
<Button variant="ghost">Leave</Button>
</Dialog.Close>
<Dialog.Close asChild>
<Button variant="primary">I am 18 or older</Button>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
);
}Props
AlertDialog
| Prop | Type | Défaut | Description |
|---|---|---|---|
cancelLabelrequis | string | — | The words of the cancel button (it takes the focus at open) — passed translated. |
confirmLabelrequis | string | — | The words of the confirm button — passed translated. |
onConfirmrequis | () => void | Promise<unknown> | — | Runs on confirm. A promise keeps the dialog open and the button loading until it settles. |
titlerequis | ReactNode | — | The question asked ("Delete this video?"): the dialog's accessible name. |
children | ReactNode | — | More content between the description and the actions. |
className | string | — | Classes of the dialog's content, merged last. |
container | HTMLElement | null | — | The element the portal renders into (default: document.body). |
defaultOpen | boolean | false | Open at first, uncontrolled. |
description | ReactNode | — | What the action does and what it costs: the dialog's description. |
onOpenChange | ((open: boolean) => void) | — | Called when it opens or closes (never while onConfirm is pending). |
open | boolean | — | Open or closed (controlled), with onOpenChange. |
tone | "danger" | "primary" | primary | danger for a destructive action (delete, leave, cancel a payment). |
trigger | ReactNode | — | The element that opens the dialog (rendered as is, asChild): a Button, an IconButton. |
DialogContent
The dialog itself, portalled over a dimmed overlay, with a close button. Give it a Dialog.Title.
| Prop | Type | Défaut | Description |
|---|---|---|---|
closeLabel | string | — | The accessible name of the close button — passed translated. |
container | HTMLElement | null | — | The element the portal renders into (default: document.body). |
dismissible | boolean | true | false: Escape and a click outside do not close it (a gate the user must answer: age, terms). It still closes
through open / onOpenChange and any Dialog.Close inside. Default true. |
hideClose | boolean | — | Leaves the close button out (the content brings its own way out, or none: see dismissible). |
placement | "center" | "bottom" | "right" | center | center · bottom (a sheet on a phone, centred from sm up) · right (a side panel, full height). |
size | "sm" | "md" | "lg" | md | sm · md · lg: the width of the dialog. |
Sheet
A sheet is a dialog anchored at the bottom (centred from sm up): Dialog.Content placement="bottom".
| Prop | Type | Défaut | Description |
|---|---|---|---|
closeLabel | string | — | The accessible name of the close button — passed translated. |
container | HTMLElement | null | — | The element the portal renders into (default: document.body). |
dismissible | boolean | — | false: Escape and a click outside do not close it (a gate the user must answer: age, terms). It still closes
through open / onOpenChange and any Dialog.Close inside. Default true. |
hideClose | boolean | — | Leaves the close button out (the content brings its own way out, or none: see dismissible). |
size | "sm" | "md" | "lg" | md | sm · md · lg: the width of the dialog. |
Accessibilité
- Radix Dialog:
role="dialog"(oralertdialog),aria-modal, named by its title and described by its description. - The focus moves into the dialog when it opens and returns to the trigger when it closes; the page behind does not scroll.
AlertDialogkeeps the focus on the cancel button first.
Clavier
| Touches | Action |
|---|---|
| Esc | Closes the dialog (unless dismissible={false}). |
| Tab / Shift+Tab | Move within the dialog: the focus is trapped while it is open. |
Bonnes pratiques
- Always a
Dialog.Title; aDialog.Descriptionwhen the title is not enough. - Name the buttons by what they do (“Send the invitation”), the cancel one by what it keeps (“Keep it”).
- Pass
closeLabeltranslated: it names the close button.
Composants liés
Générée depuis le code de @krizaka/ui 2.0.0-beta.6 : son meta.ts, ses exemples et ses types.Modifier cette documentation sur GitHub