Skip to content

Dialog and Sheet

Edit on GitHub

Modal overlays for confirmations, selections, and supplementary content. Available as centered dialogs or side-anchored sheets.

@volvo-cars/css v2.5.1@volvo-cars/react-headless v0.25.1

All modal variants use the native <dialog> element with the useDialog hook. The class on the dialog selects the visual variant.

Dialog title

This is a large dialog. On small viewports it slides up as a bottom sheet. On medium viewports and above it appears centered with a scale transition.

Close it by pressing Escape, clicking the backdrop, tapping the close button, or clicking Done.

Show codeHide code
import { useDialog } from '@volvo-cars/react-headless';
import { IconButton } from '@volvo-cars/react-icons';
export function DialogBasic() {
const { dialogProps, showDialog, closeDialog } = useDialog();
return (
<>
<button type="button" className="button-filled" onClick={showDialog}>
Open dialog
</button>
<dialog className="dialog-large" {...dialogProps}>
<header slot="header">
<div slot="close">
<IconButton
icon="x"
bleed
aria-label="Close"
variant="clear"
onClick={closeDialog}
/>
</div>
<h2 className="font-16">Dialog title</h2>
</header>
<article slot="main" className="stack-text">
<p>
This is a large dialog. On small viewports it slides up as a bottom
sheet. On medium viewports and above it appears centered with a
scale transition.
</p>
<p>
Close it by pressing Escape, clicking the backdrop, tapping the
close button, or clicking Done.
</p>
</article>
<footer slot="footer">
<div className="button-group justify-end">
<button
type="button"
className="button-filled"
onClick={closeDialog}
>
Done
</button>
</div>
</footer>
</dialog>
</>
);
}

The default modal. Slides up as a bottom sheet on small viewports, then centers with a scale animation from md upward. Uses the dialog-large class.

The default demo shows a typical large dialog with a sticky header, scrollable main content, and a footer with actions.

A compact, always-centered dialog for simple confirmations. Uses the dialog-small class. It stays centered at all viewport sizes and defaults to w-xs width.

Small dialogs typically don’t need a sticky header. Place the title and actions directly in the main slot.

A side-anchored panel that slides in from the inline-end edge on md+ viewports. On small viewports it renders as a bottom sheet, identical to dialog-large. Uses the sheet class.

Sheets anchor to the inline-end side by default (data-anchor="end"). Set data-anchor="start" to anchor to the inline-start side instead. The slide animation direction adjusts automatically and is RTL-aware.

All three variants accept width utility classes to override the default width: w-xs, w-sm, w-md, w-lg, w-xl.

Dialogs and sheets use a slot-based layout with slot attributes on direct children of the <dialog> element. All slots are optional.

SlotElementPurpose
header<header>Sticky header with a centered title
main<article>Scrollable content area
footer<footer>Sticky footer for action buttons
close<div>Floating close button that overlays content

Inside a header[slot="header"], place close and back buttons in nested div[slot="close"] and div[slot="back"] elements. The header grid auto-adjusts its layout when these are present.

Select wheels

The sticky header stays visible while scrolling and can include a back button on the start side and a close button on the end side.

The main slot scrolls independently, keeping the header and footer always visible.

Show codeHide code
import { useDialog } from '@volvo-cars/react-headless';
import { IconButton } from '@volvo-cars/react-icons';
export function DialogWithSlots() {
const { dialogProps, showDialog, closeDialog } = useDialog();
return (
<>
<button type="button" className="button-filled" onClick={showDialog}>
Open dialog
</button>
<dialog className="dialog-large" {...dialogProps}>
<header slot="header">
<div slot="back">
<IconButton
icon="chevron-back"
bleed
aria-label="Back"
variant="clear"
/>
</div>
<h2 className="font-16">Select wheels</h2>
<div slot="close">
<IconButton
icon="x"
bleed
aria-label="Close"
variant="clear"
onClick={closeDialog}
/>
</div>
</header>
<article slot="main" className="stack-text">
<p>
The sticky header stays visible while scrolling and can include a
back button on the start side and a close button on the end side.
</p>
<p>
The main slot scrolls independently, keeping the header and footer
always visible.
</p>
</article>
<footer slot="footer">
<div className="button-group justify-end">
<button
type="button"
className="button-filled"
data-color="subtle"
onClick={closeDialog}
>
Cancel
</button>
<button
type="button"
className="button-filled"
onClick={closeDialog}
>
Confirm
</button>
</div>
</footer>
</dialog>
</>
);
}

Children of [slot="main"] are constrained to the dialog’s padding by default. Add data-bleed="true" to a child to make it span the full width of the dialog, edge to edge.

Key consumer responsibilities from the WCAG audit:

  • Label the dialog: use aria-labelledby pointing to the dialog’s title, or aria-label if there is no visible title.
  • Focus management: useDialog (and native showModal()) move focus into the dialog on open and restore it on close. Don’t override this.
  • No keyboard trap: useDialog handles Escape to close when dismissible is true (the default). For non-dismissible dialogs, always provide an explicit close action.
  • Backdrop dismissal: clicking outside the dialog closes it by default. Use dismissible: false or onBeforeDismiss to control this when closing would lose user work.

The useDialog hook manages open/close state, entry/exit animations, backdrop dismissal, Escape key handling, scroll reset, and focus restoration.

See the useDialog hook documentation for the full API reference including controlled mode, non-dismissible dialogs, and dismiss interception.