Overlays
Modal
The overlay template every other one copies, built on the primitives in `@crosskit-ui/core` with no runtime dependency. Rendering is gated on presence, never on `open`, which is what keeps `data-state="closed"` on screen long enough for the exit animation to run.
data-scope="dialog"
What you get for free
- A focus trap that wraps in both directions and restores focus to the trigger
- Escape and outside-press dismissal, each independently switchable
- A shared layer stack, so only the topmost overlay answers Escape
- Body scroll lock
- Everything outside marked inert
- SSR safety — v0 called createPortal during render and threw on the server
Usage
<Modal open={open} onOpenChange={d => setOpen(d.open)} title="Delete file?">
This cannot be undone.
</Modal><Modal v-model:open="open" title="Delete file?">This cannot be undone.</Modal><Modal bind:open title="Delete file?">This cannot be undone.</Modal><ck-modal [(open)]="open" title="Delete file?">
This cannot be undone.
</ck-modal>Props
| Prop | Type | Default | Notes |
|---|---|---|---|
open | boolean | — | Controlled. v0 called this `isOpen`. |
defaultOpen | boolean | — | Uncontrolled initial state. |
onOpenChange | (d: { open }) => void | — | Reports both directions. v0 had a one-way `onClose`. |
size | "sm" | "md" | "lg" | "xl" | "full" | "md" | Max width. |
closeOnEscape | boolean | true | v0: closeOnEsc. |
closeOnInteractOutside | boolean | true | v0: closeOnBackdropClick. |
width | number | string | — | React firstExplicit width, overriding `size`. A number is read as px. |
onOk | () => void | Promise<void> | — | React firstThe confirm button. Return a promise and it holds the button busy until it settles, so a second press cannot submit twice. |
onCancel | () => void | — | React firstEvery route out the user initiated: Cancel, the close button, Escape, and a press on the mask. |
okText | ReactNode | — | React firstDefaults to the active locale's. |
cancelText | ReactNode | — | React firstDefaults to the active locale's. |
okType | "default" | "primary" | "dashed" | "text" | "link" | "primary" | React firstVisual weight of the confirm button. |
okDanger | boolean | — | React firstDestructive confirm styling. |
confirmLoading | boolean | — | React firstDrives the busy state yourself, instead of returning a promise from `onOk`. |
title | ReactNode | — | Heading; wired to aria-labelledby. |
description | ReactNode | — | Wired to aria-describedby. |
footer | ReactNode | null | — | React firstReplaces the default confirm/cancel pair. `null` removes the footer entirely. |
centered | boolean | true | Vertical centring. |
scrollable | boolean | true | Scroll long content. |
Prop names are identical in all four frameworks. Only two-way binding differs, and each framework uses its own idiom over the same underlying prop. The marked rows are the exception, and a temporary one, in both directions: React first exists in React while the other three adapters catch up, and Not in React is the prop it replaced, still live everywhere else.
Parts
| data-part | Description |
|---|---|
backdrop | The dimmed layer. |
positioner | Centres the content; carries data-centered. |
content | The dialog itself; carries data-size and data-state, and reads --ck-modal-width when `width` is set. |
title / description / body / footer | Content areas. |
Migrating from react-ui-toolkit
| v0 | v1 | Notes |
|---|---|---|
isOpen | open | |
onClose: () => void | onOpenChange: (d) => void | |
closeOnEsc | closeOnEscape | |
closeOnBackdropClick | closeOnInteractOutside | |
aria-labelledby="modal-title" | a generated id that resolves | v0's pointed at an element that did not exist. |