Components
Feedback
What the product says when something happened, is happening, or did not. Choose the component by what the reader needs to understand, not by how the message feels to write.
import { Progress, Skeleton, SkeletonText, Spinner } from "@luc/ui";
import { Toaster, toast } from "@luc/ui/components/toast";Which one
| Situation | Use | Why |
|---|---|---|
| A page or panel whose layout is known | Skeleton | It preserves the layout, so nothing jumps when the data lands. |
| A small action inside a control | Spinner | The feedback belongs in the control that was pressed, not across the page. |
| A long job with a measurable size | Progress | It answers how much longer, which a spinner cannot. |
| A wait whose end is unknown | Progress (indeterminate) | One sweeping bar for the region, never a screen full of spinners. |
| Something the page must say and keep saying | Alert | It stays where it is, and it can be announced when it arrives. |
| Context that explains a field or a chart | Callout | It is static by design and never interrupts. |
| A confirmation the reader just caused | Toaster | It leaves on its own, so it does not occupy the layout. |
| Nothing here yet, and what to do about it | EmptyState | A state the page sits in, with the action that fills it. |
| The outcome of a flow | Result | It *is* the page a submit, an import or a failure ended on. |
| A state that has a word | StatusBadge | The word is the accessible carrier; the colour is the fast one. |
| Messages that accumulated while the reader was away | NotificationCenter | An inbox with read state, not a queue that expires. |
| A surface the reader has not learned yet | GuidedTour | It points at the real thing instead of describing it. |
A wait: three shapes
The same job, three honest answers. A skeleton when the shape is known, a spinner when nothing is, a bar when the size is. They are not interchangeable and they are not a progression.
Shape known · Skeleton
Nothing known · Spinner
Validating the universe…
Size known · Progress
18 of 44 sectors
38 %
<SkeletonText lines={3} />
<Spinner size={20} label={null} />
<Progress value={38} aria-label="Position ingest" />A message: three volumes
An Alert is a status the page is telling the reader, a Callout is context that stays true, and a toast is something they just caused. Press the button: only the toast leaves on its own.
Sharpes before 60 bars are not meaningful; the panel says so above the plot.
This benchmark comes from an external provider and may be end-of-day.
<Alert tone="warning" title="Warm-up period" description="Sharpe is not meaningful before 60 bars." />
<Callout tone="info" title="Third-party data" description="This panel loads TradingView widgets." />
toast.success("Dataset saved", { description: "Validate it from the pipeline page." });An end: Result or EmptyState
A Result answers “this is what happened when you pressed the button”; an EmptyState answers “there is nothing here yet”. They look alike and are not: one is an outcome, the other is a state a page sits in.
Import finished
1,284 bars were written for BTCUSDT 1h.
No runs yet
Start one from the toolbar and its artifacts appear here.
<Result status="success" title="Import finished" description="1,284 bars written." />
<EmptyState icon={<RiInboxLine />} title="No runs yet" description="Start one from the toolbar." />A state, and its history
A badge says what is true now; the inbox says what happened while the reader was elsewhere. The badge is not an announcement, and the inbox is not a status.
Now
DegradedLiveThe badge reads as a word, carries no live region, and never expires — the panel behind the bell is where yesterday's message is still waiting.
<StatusBadge status="degraded" />
<NotificationCenter items={items} onRead={markRead} label="Notifications" />Guidelines
Do
- Say what happened, what it means and what the reader can do, in that order — a colour with no sentence is decoration.
- Match the volume to the event: a toast for what the reader just did, an alert for what the page is telling them, a result for where a flow ended.
- Announce once. A region, a control or the message itself may own the live region, but only one of them should.
Don't
- Do not use two components for one message: an alert plus a toast about the same failure reaches a screen reader twice.
- Do not show a loading state you cannot name — "Loading…" with no subject tells the reader nothing they can act on.
- Do not use colour as the only carrier of a state; every status here also has a word, an icon or a shape.
Accessibility, said once for the family
Exactly one element should announce a message. A static block renders no role until its author asks for one (`live="polite"` on an Alert), a spinner is silent when an ancestor already says it is busy (`label={null}`), a skeleton is a picture and never announced at all (`aria-hidden`, with `aria-busy` on the region), and a toast is announced by Sonner's single viewport rather than by each toast. Colour is never the only carrier: every component in this family also states the state in words, and the two moving indicators (a live dot, an indeterminate sweep) slow to a pulse under `prefers-reduced-motion` instead of freezing, because a frozen indicator reads as a bug.
Every component in this family
Inline status that stays where it is, plus `Callout` for prose-adjacent guidance.
@luc/ui/alertA determinate or indeterminate bar for work whose size is known.
@luc/uiA placeholder that reserves the shape of what is loading.
@luc/uiThe honest answer when there is nothing to show, and what to do next.
@luc/ui/empty-stateThe surface a flow ends on: what just happened, what to do next, and the action that follows from it.
@luc/uiA state as a word and a colour, with the state itself as the source of the colour.
@luc/ui/dataTransient, announced notifications with a LUC skin on Sonner's behaviour.
@luc/ui/components/toastAn inbox for the notifications a user has already been shown, with read state.
@luc/uiA multi-step tour anchored to real elements, with the steps declared as data.
@luc/ui