Skip to content

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

Choosing a feedback component
SituationUseWhy
A page or panel whose layout is knownSkeletonIt preserves the layout, so nothing jumps when the data lands.
A small action inside a controlSpinnerThe feedback belongs in the control that was pressed, not across the page.
A long job with a measurable sizeProgressIt answers how much longer, which a spinner cannot.
A wait whose end is unknownProgress (indeterminate)One sweeping bar for the region, never a screen full of spinners.
Something the page must say and keep sayingAlertIt stays where it is, and it can be announced when it arrives.
Context that explains a field or a chartCalloutIt is static by design and never interrupts.
A confirmation the reader just causedToasterIt leaves on its own, so it does not occupy the layout.
Nothing here yet, and what to do about itEmptyStateA state the page sits in, with the action that fills it.
The outcome of a flowResultIt *is* the page a submit, an import or a failure ended on.
A state that has a wordStatusBadgeThe word is the accessible carrier; the colour is the fast one.
Messages that accumulated while the reader was awayNotificationCenterAn inbox with read state, not a queue that expires.
A surface the reader has not learned yetGuidedTourIt 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 %

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.

Alert · a status
warning

Sharpes before 60 bars are not meaningful; the panel says so above the plot.

Callout · context

This benchmark comes from an external provider and may be end-of-day.

Toast · transient

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.

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

DegradedLive

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

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

AlertReviewed page

Inline status that stays where it is, plus `Callout` for prose-adjacent guidance.

@luc/ui/alert
ProgressGenerated from the public API

A determinate or indeterminate bar for work whose size is known.

@luc/ui
SpinnerGenerated from the public API

An indeterminate wait, sized to the thing it replaces.

@luc/ui
SkeletonGenerated from the public API

A placeholder that reserves the shape of what is loading.

@luc/ui
EmptyStateReviewed page

The honest answer when there is nothing to show, and what to do next.

@luc/ui/empty-state
ResultGenerated from the public API

The surface a flow ends on: what just happened, what to do next, and the action that follows from it.

@luc/ui
StatusBadgeReviewed page

A state as a word and a colour, with the state itself as the source of the colour.

@luc/ui/data
ToasterReviewed page

Transient, announced notifications with a LUC skin on Sonner's behaviour.

@luc/ui/components/toast
NotificationCenterReviewed page

An inbox for the notifications a user has already been shown, with read state.

@luc/ui
GuidedTourGenerated from the public API

A multi-step tour anchored to real elements, with the steps declared as data.

@luc/ui
Luc UI v1.0Feedback overviewdark theme