Build
Feedback
What happened, and what to show for it — toast, alert, dialog, progress, and when to confirm.
Something happened. The person needs to know. There are five ways to tell them, and the wrong one is either not noticed or in the way. Pick by what happened, not by what looks nice.
| What happened | Show | Why |
|---|---|---|
| Something succeeded, and there is nothing to do about it | Toast | Brief, out of the way, gone on its own. |
| Something failed, and there is a retry | Toast with an action | The retry is one tap away and the page is still usable. |
| Something about this page that stays true — a plan is expiring, a sync is off | Alert | Part of the page, in a tone. It does not disappear. |
| Something must be decided before going on | Dialog | It interrupts, on purpose. |
| Something is on its way | Progress or Skeleton | A bar for a known amount; a skeleton the shape of what is coming. |
| A count, or a state, on a thing — 4 unread, online | Badge | It rides on the thing. |
| A word about a control, on hover | Tooltip | Never for anything that must be read. |
Toast: brief, never blocking
Kinds
toast() is a plain call from anywhere — no component, no context hook. OmarisProvider renders the stack these land in.
<script lang="ts"> import { Button, toast } from 'omaris';</script><Button variant="tonal" onclick={() => toast('Copied to clipboard')}>Plain</Button><Button variant="tonal" tone="success" onclick={() => toast.success('Invoice sent')}>Success</Button><Button variant="tonal" tone="destructive" onclick={() => toast.error('Upload failed')}> Error</Button><Button variant="tonal" tone="warning" onclick={() => toast.warning('Card expires soon')}> Warning</Button> Most actions get no toast. One reports work that finished and left nothing on screen to see — "Copied", "Invite resent", "12 rows archived". A press whose result is visible reports itself, and a control that would only announce its own name needs no feedback at all. Toasts stack, and on a phone a handful of them are the whole viewport.
Creating something is the case people toast by reflex and should not: add a product and the product is in the list, so the list already said it. The same goes for a rename, a delete, a toggle, a filter, an opened panel and a sent message — the screen changed, which is a better confirmation than a strip of text over it. What is left, and what a toast is genuinely for:
| Toast | No toast |
|---|---|
| Copied to the clipboard — nothing on screen moved | Added a row to a list you can see |
| A request that failed, when the form cannot show it | A field that failed validation |
| Work that finished off screen — "Export ready", "42 archived" | An item deleted from the list in front of you |
| A background job that outlived the screen that started it | A dialog that closed, a page that navigated |
A toast is for a result the person did not need to wait for. It never asks a question — there is no "Are you sure?" toast — and it never carries the only copy of something important, because it will be gone in four seconds.
toast.success('Saved')for the common case;toast.error(…)for a failure.toast.promise(save(), { loading: 'Saving…', success: 'Saved', error: 'Could not save' })follows a request from start to end, in one toast.- One
actionat most — "Undo", "Retry", "View". An undo toast is the right way to make a delete safe without a dialog.
Following a promise
toast.promise shows a loading toast that becomes the success or the error one, so the three states are one call rather than three.
<script lang="ts"> import { Button, toast } from 'omaris'; const save = (ok: boolean) => new Promise<string>((resolve, reject) => setTimeout(() => (ok ? resolve('storefront') : reject(new Error('timeout'))), 1500) );</script><Button variant="tonal" onclick={() => toast.promise(save(true), { loading: 'Deploying…', success: (name) => `${name} is live`, error: 'Deploy failed' })}> Deploy</Button><Button variant="tonal" tone="destructive" onclick={() => toast.promise(save(false), { loading: 'Deploying…', success: 'Live', error: 'Deploy failed' })}> Deploy, badly</Button> Alert: stays until it is not true
Tones
An alert belongs to the page, unlike a toast, which floats over it.
Scheduled maintenance
Sunday 2–3 AM UTC.
Deploy finished
storefront is live.
Card expiring
Update it before 1 April.
Build failed
3 tests are red on main.
<script lang="ts"> import { Alert } from 'omaris';</script><div class="flex w-full flex-col gap-3"> <Alert tone="info" title="Scheduled maintenance" description="Sunday 2–3 AM UTC." /> <Alert tone="success" title="Deploy finished" description="storefront is live." /> <Alert tone="warning" title="Card expiring" description="Update it before 1 April." /> <Alert tone="destructive" title="Build failed" description="3 tests are red on main." /></div> An alert belongs to the page. It is the right answer for a condition rather than an event: the account is over its limit, the connection is offline, this record is read-only. It has a tone, an optional action, and it can be dismissed if the condition can be ignored. A destructive alert announces itself to a screen reader immediately; the rest wait their turn.
Dialog: a decision
Promise dialogs
dialog.confirm() returns a promise, so a confirmation reads as one line instead of a state variable and two callbacks. OmarisProvider renders the host these appear in.
<script lang="ts"> import { Button, dialog, toast } from 'omaris'; async function remove() { const ok = await dialog.confirm({ title: 'Delete this project?', description: 'Everything in it goes too. This cannot be undone.', confirmText: 'Delete', destructive: true }); if (ok) toast.success('Project deleted'); } async function rename() { const name = await dialog.prompt({ title: 'Rename project', value: 'storefront' }); if (name) toast('Renamed to ' + name); }</script><Button variant="tonal" tone="destructive" onclick={remove}>Delete…</Button><Button variant="tonal" onclick={rename}>Rename…</Button> A dialog stops everything, so it earns that by asking something. The provider gives you the three shapes as promises, which is how most of them should be written:
if (await dialog.confirm({ title: 'Delete this order?', destructive: true })) remove();const name = await dialog.prompt({ title: 'Rename', value: current });await dialog.alert({ title: 'Saved', description: 'Your changes are live.' }); A dialog for a task — editing, filtering, a long form — is a Sheet: it keeps the page in view and becomes a bottom sheet on a phone.
Progress: something is coming
Linear
A value when you know how far along — an upload; none when you do not, and the track animates instead of filling.
<script lang="ts"> import { Progress } from 'omaris';</script><div class="flex w-full max-w-md flex-col gap-6"> <Progress value={72} label="Uploading" /> <Progress label="Working" /> <Progress value={40} tone="success" size="lg" label="Large, success" /></div> A bar when you know how far along it is; the indeterminate bar when you only know it is working; a skeleton when what is coming has a shape. A spinner on a button (loading) is the same idea at the scale of one action. Never show two of these for one thing.
When to confirm
Confirm when the action is destructive and cannot be undone: deleting, signing out of every device, sending to a thousand people. Everywhere else, do the thing and offer an undo in the toast — a confirmation dialog on every action trains people to click through it, and then it stops protecting the one that mattered.
- Name the action in the button: "Delete order", not "OK".
- Say what will happen, once, in the description.
destructive: truepaints the confirm button red. The safe choice is the quiet one.
The whole thing on one screen
The admin pattern at /patterns/dashboard has all of it: toasts on save, an alert for a failed sync, a confirm on delete, skeletons while the table loads, and an empty state when a filter matches nothing.