Feedback
Say what happened, close to where it happened, and only interrupt for what cannot be undone. Most actions act at once and offer an Undo; a confirmation is the exception.
Which one
| When | Use | Example |
|---|---|---|
| Something just happened | Toast | Layout saved. Order PO-2210 created. |
| It can be taken back | Toast with Undo | Inventory removed from your overview. |
| It cannot be taken back | Dialog, before acting | Reset the overview? Discard the new order? |
| A state that lasts | Notice, in place | 2 invoices overdue. The carrier changed. |
| A field is wrong | Error under the field | Choose a date from May 12 on. |
Act, then offer to undo
Code
import { Button } from '@orbex/sonar-components/button'
import { ToastProvider, useToast } from '@orbex/sonar-components/toast'
import { useState } from 'react'
/* Saving says it worked; removing offers to take it back. Neither asks first. */
function Actions() {
const toast = useToast()
const [shown, setShown] = useState(true)
return (
<>
<Button variant="tonal" onClick={() => toast({ message: 'Layout saved.' })}>
Save layout
</Button>
<Button
variant="outlined"
disabled={!shown}
onClick={() => {
setShown(false)
toast({ message: 'Inventory removed from your overview.', action: { label: 'Undo', onClick: () => setShown(true) } })
}}
>
{shown ? 'Remove Inventory' : 'Inventory removed'}
</Button>
</>
)
}
export default function Undo() {
return (
<ToastProvider>
<Actions />
</ToastProvider>
)
}
Confirm what cannot be undone
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>
</>
}
/>
</>
)
}
States that last
Code
import { Button } from '@orbex/sonar-components/button'
import { Notice } from '@orbex/sonar-components/notice'
export default function Tones() {
return (
<>
<Notice title="Take a look:">
New recycled-fiber boxes are in the market.
</Notice>
<Notice tone="success" title="Order created.">
PO-2210 was sent for confirmation.
</Notice>
<Notice tone="warning" title="Carrier changed." action={<Button size="small" variant="text">Review</Button>}>
Delivery 04-121A now arrives with Atlas Logistics.
</Notice>
<Notice tone="danger" title="2 invoices overdue.">
Pay them before May 20 to keep your credit line.
</Notice>
</>
)
}
Loading
The product waits in two places, and each shows it in place, with the words of what is happening rather than an empty shape. While the search reads a query, its status line says “Understanding “…”…” and a light band in the accent green crosses the words once a second; the results replace it. While the AI assistant writes an answer, three dots pulse in its bubble, 1 s each, 150 ms apart. Cards arrive with their data, so the overview has no skeleton screens; a module that someday needs to load reuses the shimmer on its own words. With reduced motion requested, the words stay still and the dots stop.
Writing it
- Past tense for what happened (“Layout saved.”), a question for a decision (“Reset the overview?”).
- Name the thing: “Inventory removed”, not “Item removed”.
- The button that answers a question repeats its verb: Reset, Discard. Never Yes or OK.
- No exclamation marks and no apologies. Errors say how to fix them.