A focused task that keeps the user in context — new contact, insert link, leave request. Fixed header and footer with a scrolling body, five sizes, a close guard for unsaved work, and a CSS-only bottom sheet below 640px.
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogBody, DialogFooter, DialogClose, Button } from "@vamos-ui/react";
<Dialog>
<DialogTrigger asChild><Button>New contact</Button></DialogTrigger>
<DialogContent size="md">
<DialogHeader title="New contact" description="Add a contact manually." />
<DialogBody>…</DialogBody>
<DialogFooter>
<DialogClose asChild><Button>Cancel</Button></DialogClose>
<Button variant="primary">Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>Title is required for accessibility. Use srOnly when it shouldn't be visible.
Glass budget: the overlay blurs the page (--overlay-blur), so the panel drops its own blur and turns slightly more opaque. Nested layers never blur.
Close order on mobile sheets: footer buttons stack with the primary on top.
Examples
Sizes
sm 400 · md 520 · lg 680 · xl 880 px · full (viewport minus a 12px gutter on each side).
Long content
Only DialogBody scrolls; the header and footer stay put, and a hairline appears under the header once you scroll.
Unsaved changes (HR)
onBeforeClose runs on every close attempt — Esc, overlay, × and DialogClose. Return a confirm() promise to ask first.
Submitting and error
While saving: dismissible={false}, buttons locked, the primary shows loading. Errors go in an inline Alert at the top of the body.
Mobile behaviour
Below 640px: mobile="sheet" (default) anchors to the bottom, "center" stays a card, "fullscreen" is for long forms. CSS only — no component swap.
Narrow your browser below 640px, then open each one.
Menus inside a dialog
Nested layers get data-layer-depth 2 and never blur; in glass mode only the overlay does.
API
Dialog
| Prop | Type | Default |
|---|---|---|
openControlled open state. | boolean | — |
defaultOpenInitial state when uncontrolled. | boolean | false |
onOpenChangeCalled after a change is allowed (close guards run first). | (open: boolean) => void | — |
modalfalse keeps the page interactive: no overlay, no focus trap. | boolean | true |
DialogContent
| Prop | Type | Default |
|---|---|---|
sizeMaximum width: 400 / 520 / 680 / 880 px or the viewport minus 24px. Height follows content up to min(85dvh, --dialog-max-h). | "sm" | "md" | "lg" | "xl" | "full" | "md" |
mobileLayout below 640px. | "sheet" | "center" | "fullscreen" | "sheet" |
dismissiblefalse → Esc and overlay clicks don't close; the × button and DialogClose still work. | boolean | true |
onBeforeCloseRuns on every close attempt. Return or resolve false to stay open. | () => boolean | Promise<boolean> | — |
hideCloseHide the built-in × button. | boolean | false |
containerPortal target. Defaults to the nearest ThemeScope / PortalContainer, else body. | HTMLElement | null | — |
labelsBuilt-in strings (dictionary namespace "dialog"). | { close?: string } | — |
overlayClassNameClass of the overlay element. | string | — |
onOpenAutoFocusInitial focus goes to [data-autofocus], else the first focusable. preventDefault to handle it yourself. | (event: Event) => void | — |
…onEscapeKeyDown, onPointerDownOutside, onInteractOutside, forceMount… | Radix Dialog.Content props | — |
DialogHeader (LayerHeader)
| Prop | Type | Default |
|---|---|---|
titleRequired. Becomes the accessible name (aria-labelledby). | ReactNode | — |
descriptionLinked with aria-describedby; omitted cleanly when absent. | ReactNode | — |
iconLeading icon or icon well. | ReactNode | — |
actionTrailing element next to the title (badge, menu). | ReactNode | — |
srOnlyKeep the title for screen readers only. | boolean | false |
DialogBody (LayerBody)
| Prop | Type | Default |
|---|---|---|
flushRemove the padding (full-bleed lists, media). | boolean | false |
DialogFooter (LayerFooter)
| Prop | Type | Default |
|---|---|---|
alignend groups buttons at the end; between pushes the first child to the start. | "end" | "between" | "stretch" | "end" |
Other exports
| Prop | Type | Default |
|---|---|---|
DialogTrigger / DialogCloseUse asChild with a Button. | Radix parts | — |
DialogOverlayThe backdrop (DialogContent renders its own). blurred prop controls the glass blur. | component | — |
DialogTitle / DialogDescriptionFor fully custom headers. | Radix parts | — |
useDialogRoot()Close from custom code; the guard still runs. | () => { open, requestOpenChange, modal } | — |