Dialog
Modal dialog with focus trap, scroll lock, restore-on-close, and compound header/body/footer API.
Preview
Switch the framework picker (top-right of the panel) to render the same demo live in React, Vue, or Angular — same class names, ARIA, and visual output across all three.
Installation
Sisyphos UI ships unified packages for React, Vue, and Angular. Pick the one that matches your stack — every framework exports the same component classes, ARIA semantics, and CSS tokens.
$ pnpm add @sisyphos-ui/reactThen import the bundled stylesheet once at app entry: import "@sisyphos-ui/react/styles.css";
Usage
import { useState } from "react";
import { Button, Dialog } from "@sisyphos-ui/react";
export function ConfirmDelete() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)} color="error">Delete</Button>
<Dialog open={open} onOpenChange={setOpen} size="md">
<Dialog.Header>
<Dialog.Title>Delete this item?</Dialog.Title>
<Dialog.Close />
</Dialog.Header>
<Dialog.Body>
<Dialog.Description>This action cannot be undone.</Dialog.Description>
</Dialog.Body>
<Dialog.Footer>
<Button variant="outlined" onClick={() => setOpen(false)}>Cancel</Button>
<Button color="error" onClick={() => setOpen(false)}>Delete</Button>
</Dialog.Footer>
</Dialog>
</>
);
}Anatomy
The Dialog is a compound component. Compose the parts that match your use case.
<Dialog.Root>
<Dialog.Trigger />// Button that opens the dialog.
<Dialog.Overlay />// Scrim behind the dialog; closes on click.
<Dialog.Content />// Focus-trapped panel.required
<Dialog.Header />// Optional titled header slot.
<Dialog.Body />// Scrollable content region.
<Dialog.Footer />// Action buttons row.
<Dialog.Close />// Closes the dialog and restores focus.
</Dialog.Root>Sizes
`size` caps `max-width`. Use `full` to fill the viewport.
With form & initialFocus
`initialFocus` accepts a ref — the dialog focuses that element on open so keyboard users start on the first field.
Destructive confirm
Pair a dialog with a destructive action and a cautious copy — default focus stays on Cancel.
Auto-rendered close button
Set `showCloseButton` to get a close affordance in the top-right without composing `<Dialog.Close />` manually. `closeButtonLabel` overrides the aria-label.
No backdrop
Set `backdrop={false}` + `closeOnBackdropClick={false}` for contextual, in-place sheets.
API
| Prop | Type | Default | Description |
|---|---|---|---|
| open* | boolean | — | Whether the dialog is open. Always controlled. |
| onOpenChange* | (open: boolean) => void | — | Called with the next open state when the dialog requests to close. |
| size | "sm" | "md" | "lg" | "xl" | "full" | "md" | Panel width preset; `full` covers the viewport. |
| closeOnBackdropClick | boolean | true | Close when the backdrop is clicked. |
| closeOnEscape | boolean | true | Close when Escape is pressed. |
| backdrop | boolean | true | Render the scrim behind the panel; disable for transparent dialogs. |
| showCloseButton | boolean | false | Render a floating close button in the top-right; for custom placement compose `Dialog.Close` instead. |
| closeButtonLabel | string | "Close" | `aria-label` for the auto-rendered close button. |
| initialFocus | RefObject<HTMLElement> | — | Element that receives focus on open; defaults to the first focusable child. |
The full API including refs, ARIA attributes, and HTML passthroughs lives in the package README on npm.
Accessibility
Adheres to the Dialog (Modal) WAI-ARIA design pattern.
- The panel renders
role="dialog"witharia-modal="true";aria-labelledbyis wired automatically toDialog.Titleandaria-describedbytoDialog.Description. - Focus is trapped inside the open dialog (
useFocusTrap) and restored to the previously focused element on close. - Body scroll is locked while open (
useScrollLock), so background content can't be scrolled or interacted with. - The auto-rendered close button carries a configurable
aria-label(closeButtonLabel, defaults to"Close").
Keyboard interactions
| Key | Action |
|---|---|
| Tab | Moves focus to the next focusable element within the dialog. Focus is trapped. |
| Shift+Tab | Moves focus to the previous focusable element. |
| Esc | Closes the dialog and returns focus to the trigger. |