Orbex Sonar-DB Design system , home

Examples

Confirmation

Reset the overview?

Your cards go back to the default layout. Cards you added stay available in Customize.

Code
tsx
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>
          </>
        }
      />
    </>
  )
}
Side panel

Delivery 04-127C

Due today
Carrier
Iberia Freight
Window
May 12, 08:00–10:00
Pallets
24 plt
Dock
North · Gate 3
Code
tsx
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>
    </>
  )
}
Bottom sheet

New order

In pallets.

Code
tsx
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 dismissible off when a stray click outside would lose what someone typed.

Do and don’t

DoA title that asks the question, and buttons that answer it with verbs.
Don’t“Are you sure?” with OK and Cancel: people must read the text to know what OK does.
DoAsk only when the step cannot be undone. Otherwise act, and offer Undo.
Don’tA dialog for every small step: people learn to press OK without reading.

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 dismissible says.
  • 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.

tsx
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

Props of Dialog
Prop Type Default
open Required

Whether the dialog is shown. The dialog is always controlled.

boolean —
onOpenChange Required

Called with false when the person closes it: the close button, Escape, a click outside, or a swipe down.

(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.