Skip to content
omaris

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 happenedShowWhy
Something succeeded, and there is nothing to do about itToastBrief, out of the way, gone on its own.
Something failed, and there is a retryToast with an actionThe 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 offAlertPart of the page, in a tone. It does not disappear.
Something must be decided before going onDialogIt interrupts, on purpose.
Something is on its wayProgress or SkeletonA bar for a known amount; a skeleton the shape of what is coming.
A count, or a state, on a thing — 4 unread, onlineBadgeIt rides on the thing.
A word about a control, on hoverTooltipNever 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:

ToastNo toast
Copied to the clipboard — nothing on screen movedAdded a row to a list you can see
A request that failed, when the form cannot show itA 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 itA 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 action at 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.

<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: true paints 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.