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';
<Dialog
  title="Delete project"
  description="Every deployment, log and secret goes with it. This cannot be undone."
  footer={
    <>
      <Button variant="ghost" onClick={cancel}>Cancel</Button>
      <Button variant="danger" onClick={destroy}>Delete</Button>
    </>
  }
>
  <Button variant="secondary">Delete project</Button>
</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.

<Dialog
  title="Signing you in"
  description="This will not take long, and there is nothing to do about it."
  dismissible={false}
  close={false}
  width={360}
>
  <Button variant="secondary">Open a held dialog</Button>
</Dialog>

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

PropType
classNamestringAdded to the panel’s own classes, so yours wins
styleCSSPropertiesMerged 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.

PropTypeDefault
childrenReactNodeThe trigger. Leave it off and drive with open
titleReactNodeAlso the accessible name
descriptionReactNodeWired up as aria-describedby
contentReactNodeThe body
footerReactNodeActions along the bottom
openbooleanControlled
defaultOpenbooleanfalse
onOpenChange(open: boolean) => void
dismissiblebooleantrueEscape and the scrim
closebooleantrueThe close button in the corner
widthnumber440A maximum — it shrinks on a narrow screen
paddingnumber20Inside the panel
radiusnumber20
offsetnumber16Smallest distance kept from the viewport’s edges
zIndexnumber60Above the toast’s 50
containerHTMLElement | nulldocument.bodyWhere it portals to
stiffnessnumber420
dampingnumber34
openScalenumber0.96Scale the panel grows from
labelstringAccessible name, when there is no title
closeLabelstring'Close'
theme'dark' | 'light''dark'

Keyboard

Key
TabCycles inside the panel, and does not leave it
Shift + TabThe same, backwards
EscCloses, 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.