Dialog
A dialog on the native dialog element, in three placements: centered for a decision, over a scrim; at the end for a side panel beside the page, with no scrim; at the bottom for a sheet on phones. The browser handles focus.
- Native HTML
- 9.0 KB gzipped · script and styles
@orbex/sonar-components/dialog
Examples
Code
import { Button } from '@orbex/sonar-components/button'
import { Dialog } from '@orbex/sonar-components/dialog'
import { useState } from 'react'
/* Confirm only what cannot be undone. The main action goes last; the dialog closes on any choice. */
export default function Confirm() {
const [open, setOpen] = useState(false)
return (
<>
<Button variant="outlined" onClick={() => setOpen(true)}>
Reset to default
</Button>
<Dialog
open={open}
onOpenChange={setOpen}
title="Reset the overview?"
description="Your cards go back to the default layout. Cards you added stay available in Customize."
actions={
<>
<Button variant="text" onClick={() => setOpen(false)}>
Keep my layout
</Button>
<Button onClick={() => setOpen(false)}>Reset</Button>
</>
}
/>
</>
)
}
Code
import { Button } from '@orbex/sonar-components/button'
import { Dialog } from '@orbex/sonar-components/dialog'
import { Status } from '@orbex/sonar-components/status'
import { useState } from 'react'
const rows = [
['Carrier', 'Iberia Freight'],
['Window', 'May 12, 08:00–10:00'],
['Pallets', '24 plt'],
['Dock', 'North · Gate 3'],
]
export default function SidePanel() {
const [open, setOpen] = useState(false)
return (
<>
<Button variant="tonal" onClick={() => setOpen(true)}>
Delivery details
</Button>
<Dialog
open={open}
onOpenChange={setOpen}
placement="end"
modal={false}
title="Delivery 04-127C"
actions={<Button onClick={() => setOpen(false)}>Track delivery</Button>}
>
<Status tone="warning" appearance="chip">
Due today
</Status>
<dl style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '10px 24px', margin: 0, font: 'var(--sonar-type-label-md)' }}>
{rows.map(([term, value]) => (
<div key={term} style={{ display: 'contents' }}>
<dt style={{ color: 'var(--sonar-color-text-tertiary)' }}>{term}</dt>
<dd style={{ margin: 0, color: 'var(--sonar-color-text-strong)' }}>{value}</dd>
</div>
))}
</dl>
</Dialog>
</>
)
}
Code
import { Button } from '@orbex/sonar-components/button'
import { Dialog } from '@orbex/sonar-components/dialog'
import { TextField } from '@orbex/sonar-components/field'
import { useState } from 'react'
/* On phones, panels rise from the bottom, with no scrim. Typing would be lost, so a tap outside does not close this one. */
export default function Sheet() {
const [open, setOpen] = useState(false)
return (
<>
<Button variant="tonal" onClick={() => setOpen(true)}>
New order on a phone
</Button>
<Dialog
open={open}
onOpenChange={setOpen}
placement="bottom"
dismissible={false}
title="New order"
actions={<Button onClick={() => setOpen(false)}>Place order</Button>}
>
<TextField label="Product" placeholder="Name or code" />
<TextField label="Quantity" type="number" inputMode="numeric" placeholder="0" description="In pallets." />
</Dialog>
</>
)
}
Usage
- Confirm only what cannot be undone, such as resetting a layout. Everything else gets an undo in a toast.
- The title asks the question; the main action answers it with a verb, last in the row.
- Panels open at the end on tablets and desktops, from 600 px wide (an iPad mini in portrait keeps them at the side), and from the bottom on phones, 44 px from the top of the window. On a tablet this departs from the product’s 768 px size class, on purpose: the side panel keeps the overview in view. On desktops a side panel slides in 48 px with a fade, as in the product; on tablets it slides all the way in from the right edge, as Navigation by device defines.
- Nothing in a panel or sheet shows the system’s scrollbar: its body scrolls with the product’s thin indicator, 4 px from the edge.
- A side panel is not modal (
modal={false}): it starts under the top bar, has no scrim, and the overview stays in view with the row it came from marked. Only decisions, such as a confirmation, take a scrim. - The six panels the overview opens are gathered, live, in Patterns › Panels.
- Turn
dismissibleoff when a stray click outside would lose what someone typed.
Do and don’t
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab and Shift Tab | Move among the dialog’s controls only; the page behind is inert. |
| Escape | Closes the dialog and returns focus to what opened it. |
Roles and attributes
| Element or attribute | What it gives |
|---|---|
<dialog> opened with showModal() |
The browser moves focus in, keeps it inside and makes the rest of the page inert. |
aria-labelledby, aria-describedby |
The title names the dialog; the description is read after it. |
| Close button | Named by closeLabel, “Close” by default. |
| Body that scrolls (side panels and sheets) | A group named by the title while it scrolls, so the keyboard can scroll it; the indicator beside it is drawn and hidden. |
Notes
- Modal:
showModal()moves focus into the dialog, keeps it there, makes the rest of the page inert and returns focus to the trigger on close. - Side panel: focus moves into the panel when it opens and back to the trigger when it closes. Tab and Shift+Tab go round inside it, so focus never lands on the page behind; the page stays in view and a click on it still works. On phones, where it is a sheet over the page, the rest of the page is inert while it is open. Escape or a click outside closes it.
- The title names the dialog and the description is read with it.
- Escape and the close button always close it, whatever
dismissiblesays. - With reduced motion, the dialog fades without moving.
Use it
Each component is its own entry point with its own stylesheet, so an app loads only what it uses. The tokens and the cascade layers are set once for the app: see Installation.
import { DialogHost, Dialog } from '@orbex/sonar-components/dialog'
import '@orbex/sonar-components/dialog.css'
API
DialogHost
Holds the side panels and sheets written inside it, as a drawn app frame does, instead of placing them against the window.
Dialog
| Prop | Type | Default |
|---|---|---|
open
Required
Whether the dialog is shown. The dialog is always controlled. |
boolean
|
— |
onOpenChange
Required
Called with |
(open: boolean) => void
|
— |
title
Required
The heading, which also names the dialog for assistive technology. |
ReactNode
|
— |
description
One paragraph under the title, read when the dialog opens. |
ReactNode
|
— |
placement
Center for a decision; end for a side panel, such as the details of a delivery; bottom for a sheet on phones. On phones (under 600 px) a side panel becomes a sheet from the bottom, with a handle to swipe it down; tablets and desktops keep it at the side. |
'center' | 'end' | 'bottom'
|
'center' |
modal
Whether the rest of the page waits behind a scrim. A side panel that sits beside the overview, such as the details of a delivery or a new order, is not modal: no scrim, the page stays in view and the row it came from stays marked. It starts under the top bar (--sonar-dialog-top) and closes on Escape or a click outside. |
boolean
|
true |
actions
Buttons at the foot of the dialog, the main action last. |
ReactNode
|
— |
closeLabel
Name of the close button. |
string
|
'Close' |
dismissible
Whether a click on the scrim, or outside a panel that is not modal, closes it. Turn it off when closing would lose what the person typed. |
boolean
|
true |
Also takes every attribute of the native <dialog>
, except open | title | onClose | onCancel, and a ref to it.