Dialog and Sheet
Modal overlays for confirmations, selections, and supplementary content. Available as centered dialogs or side-anchored sheets.
All modal variants use the native <dialog> element with the useDialog hook. The class on the dialog selects the visual variant.
Show 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> </> );}Variants
Section titled “Variants”Dialog large
Section titled “Dialog large”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.
Dialog small
Section titled “Dialog small”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.
Anchor side
Section titled “Anchor side”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.
Content slots
Section titled “Content slots”Dialogs and sheets use a slot-based layout with slot attributes on direct children of the <dialog> element. All slots are optional.
| Slot | Element | Purpose |
|---|---|---|
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 |
Header with back and close buttons
Section titled “Header with back and close buttons”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.
Show 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> </> );}Full-bleed content
Section titled “Full-bleed content”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.
Accessibility
Section titled “Accessibility”Key consumer responsibilities from the WCAG audit:
- Label the dialog: use
aria-labelledbypointing to the dialog’s title, oraria-labelif there is no visible title. - Focus management:
useDialog(and nativeshowModal()) move focus into the dialog on open and restore it on close. Don’t override this. - No keyboard trap:
useDialoghandles Escape to close whendismissibleistrue(the default). For non-dismissible dialogs, always provide an explicit close action. - Backdrop dismissal: clicking outside the dialog closes it by default. Use
dismissible: falseoronBeforeDismissto 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.