Components
Modal
An accessible dialog with focus trap, ESC-to-close, and overlay click handling. Built on Radix Dialog primitives. Controlled via open / onClose; supports title, optional description, footer slot, and children body.
Installation
terminal
npx shadcn@latest add @wuko/modalUsage
Modal is fully controlled. Hold the open state in your component, render the trigger separately, and pass open and onClose down. Radix handles focus trap, ESC, click-outside, and ARIA wiring. Your code just decides when the dialog is visible.
components/delete-project-dialog.tsx
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | — | Required. Whether the dialog is visible. Modal is fully controlled. Manage this state in your component. |
| onClose | () => void | — | Called when the user dismisses the dialog (close button, ESC, or backdrop click). Without this, the modal cannot be dismissed. |
| title | string | — | Required. Header title. Wired to aria-labelledby on the dialog content. |
| description | string | — | Optional descriptive text rendered below the title. When passed, wired to aria-describedby. Strongly recommended for confirmations and destructive actions. |
| footer | ReactNode | — | Footer region, typically action buttons. Right-aligned with a top divider. |
| children | ReactNode | — | Body content. Renders below the description (if any) and above the footer. |
| className | string | — | Tailwind classes merged onto the dialog content (the panel). Does not target the overlay. |
Accessibility
- Focus trap.While open, keyboard focus is trapped inside the dialog. Tab and Shift-Tab cycle through the dialog's focusable elements only. Implemented by Radix's
FocusScopeprimitive. - Auto-focus on open. Focus moves to the first focusable element inside the dialog when it opens. On close, focus returns to the element that triggered the open (typically the open button).
- ESC and click-outside close. Pressing Escape or clicking the backdrop both dismiss the dialog. Both fire
onClose. - Body scroll lock. While the dialog is open, the page behind the backdrop cannot be scrolled. Restored on close.
- ARIA wiring. The dialog has
role="dialog"andaria-modal="true". Thetitleprop is wired asaria-labelledby. When you passdescription, it is wired asaria-describedby; when you omit it,aria-describedbyis set toundefinedexplicitly to suppress Radix's dev-mode warning. - When to pass
description. Pass it for confirmations, destructive actions, and any dialog where a screen-reader user needs context beyond the title. For trivial dialogs (e.g., a brief acknowledgement), title + children is acceptable. - Close button. The X close button has
aria-label="Close". The X svg isaria-hidden; screen readers announce only the label. - Consumer responsibility. You render the trigger (typically a Button) and manage
openstate. Modal handles everything inside the dialog (focus, ARIA, dismiss, scroll lock) so the consumer doesn't need to wire those up.