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

PropTypeDefaultNotes
openbooleanControlled. v0 called this `isOpen`.
defaultOpenbooleanUncontrolled initial state.
onOpenChange(d: { open }) => voidReports both directions. v0 had a one-way `onClose`.
size"sm" | "md" | "lg" | "xl" | "full""md"Max width.
closeOnEscapebooleantruev0: closeOnEsc.
closeOnInteractOutsidebooleantruev0: closeOnBackdropClick.
widthnumber | stringReact 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() => voidReact firstEvery route out the user initiated: Cancel, the close button, Escape, and a press on the mask.
okTextReactNodeReact firstDefaults to the active locale's.
cancelTextReactNodeReact firstDefaults to the active locale's.
okType"default" | "primary" | "dashed" | "text" | "link""primary"React firstVisual weight of the confirm button.
okDangerbooleanReact firstDestructive confirm styling.
confirmLoadingbooleanReact firstDrives the busy state yourself, instead of returning a promise from `onOk`.
titleReactNodeHeading; wired to aria-labelledby.
descriptionReactNodeWired to aria-describedby.
footerReactNode | nullReact firstReplaces the default confirm/cancel pair. `null` removes the footer entirely.
centeredbooleantrueVertical centring.
scrollablebooleantrueScroll 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-partDescription
backdropThe dimmed layer.
positionerCentres the content; carries data-centered.
contentThe dialog itself; carries data-size and data-state, and reads --ck-modal-width when `width` is set.
title / description / body / footerContent areas.

Migrating from react-ui-toolkit

v0v1Notes
isOpenopen
onClose: () => voidonOpenChange: (d) => void
closeOnEsccloseOnEscape
closeOnBackdropClickcloseOnInteractOutside
aria-labelledby="modal-title"a generated id that resolvesv0's pointed at an element that did not exist.