Overlay
Dialog
A modal panel. Focus is trapped inside it, the page is held still behind it, and it is the one overlay in the library that does not grow from an anchor — because it is not anchored to anything.
Usage
import { Dialog } from '@carabine/ui/dialog';Centred, and it stays centred. Everything else in this library grows out of the corner nearest what opened it; a dialog has no anchor to grow from, so it scales from its middle at 0.96 and nowhere else.
Anatomy
<div> // the scrim, and the page behind it is held still
<div role="dialog"> // the panel: glass shell, lit core, focus trapped
{title} // also the accessible name
{description} // wired up as aria-describedby
{content} // the body
{footer} // the row is ours, the buttons are yours
<button /> // the close, in the corner
</div>
</div>The footer row is the component's and the buttons in it are yours. A dialog that shipped its own confirm and cancel would ship the wrong words, in the wrong order, in the wrong language.
The panel scrolls, not the page: a dialog taller than the window has to stay reachable with everything behind it still held.
Examples
One that will not close
dismissible={false} turns off Escape and the scrim; close takes away the corner button. Use it for the moment where leaving would break something, and give the reader a way out inside the panel — a dialog with no exit at all is a bug wearing a design.
Customization
<Dialog
width={560} // a maximum — it shrinks on a narrow screen
padding={24}
radius={24}
offset={16} // smallest distance kept from the window's edges
zIndex={60} // above the toast's 50, because it is above the toast
openScale={0.96} // nothing appears from scale(0)
theme="light"
/>Styling reference
Tailwind utilities plus dialog.css, which carries the panel's scrollbar. The material is the toast's — a glass shell around a core with a top-lit gradient, hairlines as inset shadows rather than borders. A dialog and a toast on the same screen are the same object at two sizes.
API reference
| Prop | Type | |
|---|---|---|
| className | string | Added to the panel’s own classes, so yours wins |
| style | CSSProperties | Merged after the component’s own inline styles, so yours wins |
Both land on the panel. The trigger is already your element — you style it where you write it — and the panel is the part that portals away from your markup and is otherwise out of reach. State is legible from CSS as well — data-state, data-disabled, data-side — so a rule can answer it without knowing a single class name of ours. See Theming.
| Prop | Type | Default | |
|---|---|---|---|
| children | ReactNode | — | The trigger. Leave it off and drive with open |
| title | ReactNode | — | Also the accessible name |
| description | ReactNode | — | Wired up as aria-describedby |
| content | ReactNode | — | The body |
| footer | ReactNode | — | Actions along the bottom |
| open | boolean | — | Controlled |
| defaultOpen | boolean | false | |
| onOpenChange | (open: boolean) => void | — | |
| dismissible | boolean | true | Escape and the scrim |
| close | boolean | true | The close button in the corner |
| width | number | 440 | A maximum — it shrinks on a narrow screen |
| padding | number | 20 | Inside the panel |
| radius | number | 20 | |
| offset | number | 16 | Smallest distance kept from the viewport’s edges |
| zIndex | number | 60 | Above the toast’s 50 |
| container | HTMLElement | null | document.body | Where it portals to |
| stiffness | number | 420 | |
| damping | number | 34 | |
| openScale | number | 0.96 | Scale the panel grows from |
| label | string | — | Accessible name, when there is no title |
| closeLabel | string | 'Close' | |
| theme | 'dark' | 'light' | 'dark' |
Keyboard
| Key | |
|---|---|
| Tab | Cycles inside the panel, and does not leave it |
| Shift + Tab | The same, backwards |
| Esc | Closes, unless dismissible is off |
Focus goes to the panel on open and returns to whatever opened it on close — including when the trigger has been removed in the meantime, in which case it falls back to the body rather than to nothing.