Dialog and Sheet
All three modal types use the native <dialog> element and the useDialog hook from @volvo-cars/react-headless. The CSS class on the <dialog> determines the variant.
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 example above shows the 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.
import { useDialog } from '@volvo-cars/react-headless';import { IconButton } from '@volvo-cars/react-icons';
export function DialogSmallExample() { const { dialogProps, showDialog, closeDialog } = useDialog();
return ( <> <button type="button" className="button-filled" onClick={showDialog}> Open small dialog </button> <dialog className="dialog-small" {...dialogProps}> <div slot="close"> <IconButton bleed icon="x" aria-label="Close" variant="clear" onClick={closeDialog} /> </div> <article className="stack-text" slot="main"> <h2 className="font-16 font-medium"> Are you sure you want to proceed? </h2> <p className="text-secondary"> All associated data will be permanently deleted and you will not be able to recover it. </p> <div className="button-group"> <button type="button" className="button-filled" data-color="subtle" onClick={closeDialog} > Cancel </button> <button type="button" className="button-filled" onClick={closeDialog} > Continue </button> </div> </article> </dialog> </> );}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.
import { useDialog } from '@volvo-cars/react-headless';import { IconButton } from '@volvo-cars/react-icons';
export function SheetExample() { const { dialogProps, showDialog, closeDialog } = useDialog();
return ( <> <button type="button" className="button-filled" onClick={showDialog}> Open sheet </button> <dialog className="sheet" {...dialogProps} data-anchor="end"> <header slot="header"> <div slot="close"> <IconButton bleed icon="x" aria-label="Close" variant="clear" onClick={closeDialog} /> </div> <h2 className="font-16">Equipment level</h2> </header> <article className="stack-text" slot="main"> <h3 className="heading-2">Ultra</h3> <p> Experience the highest equipment level for optimum comfort, advanced connectivity and outstanding Scandinavian style. </p> <p> Includes premium sound system, head-up display, and full leather interior with ventilated seats. </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} > Compare </button> </div> </footer> </dialog> </> );}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.
import { useDialog } from '@volvo-cars/react-headless';import { IconButton } from '@volvo-cars/react-icons';
export function SheetAnchorStart() { const { dialogProps, showDialog, closeDialog } = useDialog();
return ( <> <button type="button" className="button-filled" onClick={showDialog}> Open start-anchored sheet </button> <dialog className="sheet" {...dialogProps} data-anchor="start"> <header slot="header"> <div slot="close"> <IconButton icon="x" bleed aria-label="Close" variant="clear" onClick={closeDialog} /> </div> <h2 className="font-16">Navigation</h2> </header> <article slot="main" className="stack-16"> <p> Set <code>data-anchor="start"</code> to anchor the sheet to the inline-start side of the viewport. The default anchor is{' '} <code>end</code>. </p> </article> </dialog> </> );}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.
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.
import { useDialog } from '@volvo-cars/react-headless';import { IconButton } from '@volvo-cars/react-icons';
export function DialogFullBleed() { 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 bleed icon="x" aria-label="Close" variant="clear" onClick={closeDialog} /> </div> <h2 className="font-16">Gallery</h2> </header> <article slot="main" className="stack-24"> <p>Content within the default padding.</p> <img data-bleed="true" src="/images/wheels.webp" className="img" alt="18 inch 8 spoke aero wheels in silver metallic finish" /> <p>More content after the full-bleed image.</p> </article> <footer slot="footer"> <div className="button-group justify-end"> <button type="button" className="button-filled" onClick={closeDialog} > Done </button> </div> </footer> </dialog> </> );}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.