Skip to content

Status & feedback

Signaling state, success, error, activity. Spinners, skeletons, alerts, KPIs.

What this category is

Components that communicate state to the user — what happened, what's happening, and what isn't there. They group by intent:

  • Messages (success/error/warning): Alert (inline), Banner (top of page), Toast (transient) — three scopes of the same concept.
  • Status labels: Badge (fixed status), Tag (removable chip), Stat (KPI).
  • Activity/loading: Spinner, Progress, Skeleton, NProgressBar (fixed top bar).
  • Empty/error screen states: EmptyState, ErrorState.

When to use: whenever the user needs to know the outcome of an action or the state of some data. The golden rule: never leave an async action without feedback — show Spinner/Skeleton during, Toast/Alert on completion.

Alert vs Banner vs Toast — pick by scope

Alert is inline and stays until the user resolves the context. Banner is persistent at the top and applies to the whole page/app. Toast is transient and dismisses itself. Use Toast for quick confirmations, Alert for errors tied to a field/section, Banner for global notices (maintenance, expiration).

Alert

Alert · Timeline · BottomSheet in the gallery

Section feedback-extra of the gallery — run it locally to interact.

When to use: a message tied to a specific context on the screen (form error, result of an operation in a section). Visible until the condition changes.

An inline banner. Different from Banner (top-of-page) and Toast (transient).

import { useState } from "react";
import { Alert } from "tempest-react-sdk";
import { AlertCircle } from "lucide-react";

export function Avisos() {
    const [open, setOpen] = useState(true);

    return (
        <>
            <Alert variant="success" appearance="soft" title="Saved">
                Your changes were applied successfully.
            </Alert>

            {open && (
                <Alert
                    variant="danger"
                    appearance="outline"
                    icon={<AlertCircle size={16} />}
                    onClose={() => setOpen(false)}
                >
                    Could not process the payment.
                </Alert>
            )}
        </>
    );
}
Prop Type Default
variant "neutral" \| "info" \| "success" \| "warning" \| "danger" "info"
appearance "soft" \| "solid" \| "outline" "soft"
title ReactNode —
icon ReactNode —
description ReactNode —
onClose () => void (renders the close button) —
closeLabel string —

A11y: role="status" for info/success, role="alert" for warning/danger.

Stat · Tag · Money · Banner in the gallery

Section data-display of the gallery — run it locally to interact.

Persistent, top-of-page. Use it for environment indicators, maintenance, expiration.

import { useState } from "react";
import { Banner, Button } from "tempest-react-sdk";

export function AvisoDeAssinatura() {
    const [open, setOpen] = useState(true);
    if (!open) return null;

    return (
        <Banner
            variant="warning"
            dismissible
            onDismiss={() => setOpen(false)}
            action={<Button size="sm">Renew</Button>}
        >
            Your subscription expires in 3 days.
        </Banner>
    );
}

Same variants as Alert.

Badge

Badges · Cards · Skeleton in the gallery

Section feedback of the gallery — run it locally to interact.

When to use: a short, read-only status label next to an item (order status, count). For a chip the user removes, use Tag.

Status pill — not removable.

import { Badge } from "tempest-react-sdk";

export function Selos() {
    return (
        <>
            <Badge>Default</Badge>
            <Badge variant="success">Paid</Badge>
            <Badge variant="danger" appearance="solid">
                Overdue
            </Badge>
            <Badge variant="warning" appearance="outline" shape="square">
                Pending
            </Badge>
            <Badge variant="info" dot>
                3
            </Badge>
        </>
    );
}
Prop Type Default
variant "neutral" \| "primary" \| "success" \| "warning" \| "danger" \| "info" "neutral"
appearance "soft" \| "solid" \| "outline" "soft"
shape "pill" \| "square" "pill"
size "sm" \| "md" \| "lg" "md"
dot boolean (status indicator + number) false

Tag

A removable chip. Use it for filter tokens, applied filters, selected entities.

import { Tag } from "tempest-react-sdk";

export function Filtros({ removeFilter }: { removeFilter: (key: string) => void }) {
    return (
        <>
            <Tag onRemove={() => removeFilter("sp")} variant="primary">
                São Paulo
            </Tag>
            <Tag size="sm">In stock</Tag>
        </>
    );
}
Prop Type Default
variant "neutral" \| "primary" \| "success" \| "warning" \| "danger" \| "info" "neutral"
size "sm" \| "md" \| "lg" "md"
onRemove () => void —
removeLabel string (a11y) "Remover"

Stat

When to use: highlight a single metric with its delta (revenue, sessions, NPS) on dashboards. For several KPIs side by side, combine with Grid.

A KPI card for dashboards.

import { Stat } from "tempest-react-sdk";
import { TrendingUp } from "lucide-react";

export function Metricas() {
    return (
        <>
            <Stat
                label="Revenue"
                value="R$ 12.345"
                delta="+12,4%"
                hint="vs. previous month"
                icon={<TrendingUp size={16} />}
            />
            <Stat label="Sessions" value="1.2k" delta="-3%" hint="last 7 days" />
            <Stat label="NPS" value="78" delta="0" trend="flat" />
        </>
    );
}
Prop Type Default
label ReactNode —
value ReactNode —
delta ReactNode —
trend "up" \| "down" \| "flat" inferred from +/- in the delta string
hint ReactNode —
icon ReactNode —

Progress

Stepper · Progress · VirtualList in the gallery

Section advanced of the gallery — run it locally to interact.

Progress bar.

import { Progress } from "tempest-react-sdk";

export function Barras({ uploadProgress }: { uploadProgress: number }) {
    return (
        <>
            <Progress value={uploadProgress} max={100} variant="primary" />
            <Progress value={100} variant="success" />
            <Progress indeterminate variant="primary" />
        </>
    );
}
Prop Type Default
value number —
max number 100
variant "primary" \| "success" \| "danger" "primary"
indeterminate boolean false

NProgress

When to use: a fixed top loading bar for transitions with no exact position — route navigation, background requests. For deterministic upload progress, prefer Progress.

An imperative controller (nprogress) + the visual bar (<NProgressBar>). Mount the bar once near the app root and drive the controller from anywhere (router transitions, fetch interceptors).

import { Button, NProgressBar, nprogress } from "tempest-react-sdk";

async function loadPage(): Promise<void> {
    nprogress.start();
    try {
        await fetch("/api/data");
    } finally {
        nprogress.done();
    }
}

export function Raiz() {
    return (
        <>
            <NProgressBar color="var(--tempest-primary)" height={3} />
            <Button onClick={loadPage}>Load</Button>
        </>
    );
}
nprogress Signature What it does
start () => void Shows the bar and starts trickling toward ~0.9.
done () => void Completes to 1 and hides the bar shortly after.
set (n: number) => void Sets progress explicitly (clamped 0..1).
inc (amount?: number) => void Increments progress by amount.
Prop (NProgressBar) Type Default
color string var(--tempest-primary)
height number 3
className string —

Great with router navigation

Call nprogress.start() when a transition begins and nprogress.done() when the route mounts. nprogress is a module-level singleton — no provider or props drilling needed.

A11y: the bar uses role="progressbar" with aria-valuenow reflecting the percentage.

Spinner

A generic loader.

import { Spinner } from "tempest-react-sdk";

export function Carregando() {
    return (
        <>
            <Spinner />
            <Spinner size="lg" />
            <Spinner size="lg" caption="Loading…" />
            <Spinner overlay caption="Loading…" />
        </>
    );
}
Prop Type Default Description
size "xs" \| "sm" \| "md" \| "lg" \| "xl" "md" Indicator size.
label string "Carregando" Accessible label (screen readers).
caption ReactNode — Visible text under the spinner.
overlay boolean false Centers inside a full-area container (position: absolute).

Back-compat

<Spinner /> with no caption/overlay stays a single <span role="status"> — existing usage is unchanged.

overlay positions against the nearest positioned ancestor

overlay applies position: absolute; inset: 0, so it covers the first ancestor whose position is not static. If nothing above it is positioned, the spinner escapes to the topmost positioned container — in practice, the whole page. Give position: relative to the block that should be covered.

Skeleton

When to use: loading content whose shape is already known (cards, table rows, avatar). It reduces layout shift. For shapeless loading (a button processing), use Spinner.

A placeholder with shimmer while data loads.

Skeleton mirrors the final layout

Give the skeleton the same dimensions/proportions as the real content — that is what eliminates layout shift when the data arrives. Skeletons that are too generic (a single block) confuse more than they help.

import { Skeleton, Stack } from "tempest-react-sdk";

export function Esqueleto({ loading }: { loading: boolean }) {
    if (loading) {
        return (
            <Stack gap={2}>
                <Skeleton variant="text" width="60%" />
                <Skeleton variant="text" width="40%" />
                <Skeleton variant="rect" height={120} />
                <Skeleton variant="circle" width={40} height={40} />
            </Stack>
        );
    }
    return <p>Loaded content</p>;
}
Prop Type Default
variant "rect" \| "text" \| "circle" "rect"
width number \| string "100%"
height number \| string —

The skeleton has to have the shape of the real content

The value of Skeleton is not the animation, it is the reserved space: if the ghost lines are close to the width and height of what lands there, the text does not jump when the data arrives. A generic 200px block that becomes a 340px card trades a spinner for a layout shift — the very problem the skeleton existed to solve.

RefreshIndicator

Material (ListTile · FAB · Rail) in the gallery

Section material of the gallery — run it locally to interact.

When to use: give the user a pull-to-refresh gesture on scrollable mobile lists/screens (touch) — feeds, inboxes, dashboards. It wraps the scrollable content and fires onRefresh when the user pulls past the threshold and releases.

It is a touch gesture — there's no mouse equivalent. The SDK Spinner shows while pulling and during the refresh.

import { RefreshIndicator } from "tempest-react-sdk";

interface FeedItem {
    id: string;
    title: string;
}

export function Feed({ items, refetch }: { items: FeedItem[]; refetch: () => Promise<void> }) {
    return (
        <RefreshIndicator onRefresh={refetch}>
            <ul>
                {items.map((item) => (
                    <li key={item.id}>{item.title}</li>
                ))}
            </ul>
        </RefreshIndicator>
    );
}
Prop Type Default
onRefresh () => void \| Promise<void> —
children ReactNode (scrollable area) —
threshold number (px before firing) 80
disabled boolean false

Touch-only gesture

RefreshIndicator listens for touch drags that start with the container scrolled to the top — it doesn't respond to the mouse. On desktop, offer an explicit "Refresh" button as a fallback.

Toast

When to use: a transient confirmation of an action that already finished ("Saved", "Item removed") — it needs no attention and dismisses itself. For errors the user must resolve, prefer Alert/Banner (which stay).

Transient notifications. Set up via ToastProvider + use via useToast().

Toast is not for critical errors

Toasts disappear in a few seconds — if the user must act on the message (fix a field, retry), it has to persist. Use Toast only for dismissible feedback; actionable errors go in Alert/ErrorState.

import { ToastProvider, useToast } from "tempest-react-sdk";

function Salvar() {
    const toast = useToast();

    return (
        <button
            onClick={() => {
                toast.success("Saved");
                toast.error("Could not process the payment.", { duration: 8000 });
                toast.show({
                    title: "Sync",
                    description: "In progress…",
                    variant: "info",
                });
            }}
        >
            Save
        </button>
    );
}

export function App() {
    return (
        <ToastProvider position="top-right" defaultDuration={4000}>
            <Salvar />
        </ToastProvider>
    );
}
ToastApi Signature
show (options: ToastOptions) => string (returns id)
success (description, options?) => string
error (description, options?) => string
warning (description, options?) => string
info (description, options?) => string
dismiss (id: string) => void

ToastProvider.position: "top-right" (default), "top-left", "top-center", "bottom-right", "bottom-left", "bottom-center".

Safe-area aware.

Icon and alignment

ToastOptions.icon takes any ReactNode — usually an app asset — and renders it in its own column to the left of the text. An <img> or <svg> inside it is capped at 32px without being distorted, at any depth. A node that renders nothing (null, false, [], an empty fragment) counts as no icon.

import { ToastProvider, useToast } from "tempest-react-sdk";

function Achievement() {
    const toast = useToast();

    return (
        <button
            onClick={() =>
                toast.show({
                    icon: <img src={"/assets/invict.svg"} alt="" />,
                    title: "Undefeated title unlocked!",
                    variant: "success",
                    duration: 4000,
                    align: "center",
                })
            }
        >
            Unlock
        </button>
    );
}

export function App() {
    return (
        <ToastProvider>
            <Achievement />
        </ToastProvider>
    );
}
Option Type Default What it does
icon ReactNode — Slot to the left of the text, for an icon. inert; decorative (aria-hidden) without iconLabel.
iconLabel string — Accessible name for the icon: the slot becomes role="img" and is announced with the toast.
align "start" \| "center" "start" Alignment of the title and description.

The icon is decorative by default because it almost always repeats the title ("Undefeated title unlocked!" beside the medal). Otherwise an unlabelled image inside the aria-live region would be read as "image" or by its file name. When the icon carries information the text does not, pass iconLabel — no need to touch the asset's alt.

The slot is for an icon, not a control: the wrapper is inert, so a button or link placed there gets neither focus nor clicks. An action on the notification lives outside the toast — if the user must act, use Alert.

Why the text is not centred by default

A toast reads as a sentence beside a close button, and it is usually stacked with others. Left-aligned, the text shares one edge with the icon and with the neighbouring toasts, and a two-line description stays scannable — centred, every line starts somewhere else. align: "center" is for the short, celebratory one-line toast, like the one in the example.

variant, duration and the queue do not change with an icon: the coloured border still marks the variant, and a toast without an icon keeps exactly the previous layout.

useToast only exists under a <ToastProvider>

Outside one the hook throws useToast must be used inside a <ToastProvider>. The provider belongs at the app root, above the router — if it sits inside a route, any toast fired from outside that route takes the screen down instead of showing up.

EmptyState

Table & Pagination in the gallery

Section table of the gallery — run it locally to interact.

When to use: a list/collection returned zero items successfully (no orders yet, search with no results). Always offer a way out (create the first item, clear filters). Don't confuse it with an error — for a failure use ErrorState.

A centered "nothing here".

import { Button, EmptyState } from "tempest-react-sdk";
import { Inbox, Plus } from "lucide-react";

export function SemPedidos() {
    return (
        <EmptyState
            icon={<Inbox size={32} />}
            title="No orders yet"
            description="When your customers place orders, they show up here."
            action={<Button leftIcon={<Plus size={16} />}>New order</Button>}
        />
    );
}

An empty state is not an error state

EmptyState is for "the query worked and there is nothing" — the suggested action creates the first record. ErrorState is for "the query failed", and the action is try again. Swapping one for the other makes the user create a duplicate order when what actually broke was the network.

ErrorState

When to use: an operation failed (a request errored, an exception) and the user can retry. Unlike EmptyState, which represents success with no data.

A failure with a retry button.

import { ErrorState } from "tempest-react-sdk";

export function FalhaAoCarregar({
    error,
    refetch,
}: {
    error: unknown;
    refetch: () => void;
}) {
    return (
        <ErrorState
            title="Could not load"
            description={String(error)}
            onRetry={refetch}
        />
    );
}

OfflineIndicator

When to use: warn that the app is offline and confirm when the connection returns. Driven by useOnline — renders nothing while online, so mount it at the root without an if.

import { OfflineIndicator } from "tempest-react-sdk";

export function Raiz() {
    return <OfflineIndicator position="top" />;
}

position: "top" | "bottom" (default). Pass children to replace the body, or onlineFlashMs={0} to skip the confirmation flash. See PWA & Offline-First.

SyncStatusBadge

PWA: Install · Push in the gallery

Section pwa of the gallery — run it locally to interact.

When to use: show the offline engine state (synced / syncing / pending / offline / error). Two modes: pass sync to connect straight to the engine (zero wiring), or pass an explicit tone (presentational, testable without IndexedDB).

import { SyncStatusBadge, useSyncStatus, type OfflineSync } from "tempest-react-sdk";

interface Nota {
    id: string;
    body: string;
}

export function Status({ notesSync }: { notesSync: OfflineSync<Nota> }) {
    const { tone, pending } = useSyncStatus(notesSync);

    return (
        <>
            <SyncStatusBadge sync={notesSync} />
            <SyncStatusBadge tone={tone} pending={pending} />
        </>
    );
}

tone: "idle" | "syncing" | "pending" | "offline" | "error". iconOnly hides the label; labels overrides the per-tone text.

UpdatePrompt

When to use: announce a new app version (service worker) and let the user reload. Pairs with useServiceWorkerUpdate.

import { UpdatePrompt, useServiceWorkerUpdate } from "tempest-react-sdk";

export function AvisoDeAtualizacao() {
    const { updateAvailable, applyUpdate } = useServiceWorkerUpdate({ url: "/sw.js" });

    return <UpdatePrompt open={updateAvailable} onUpdate={applyUpdate} />;
}

Renders nothing when open={false}. onDismiss (optional) shows the dismiss button; position "top"/"bottom".

General A11y

  • Alert/Banner with the warning/danger variant use role="alert" (announced immediately).
  • The Toast container is aria-live="polite" + aria-atomic="false" — only the new toast is announced, not the whole stack. An icon without iconLabel is aria-hidden.
  • Spinner/Progress.indeterminate add aria-busy="true".
  • Skeleton is decorative — it doesn't announce (aria-hidden).
  • Stat.value with tabular-nums aligns numeric values in columns.

Recap

  • Never leave an async action without feedback: Spinner/Skeleton during, Toast/Alert on completion.
  • Messages by scope: Alert (inline/contextual), Banner (global/top), Toast (transient).
  • EmptyState = success with no data; ErrorState = failure with retry. Don't swap one for the other.
  • Toast only for dismissible feedback; actionable errors stay in Alert/ErrorState.

Related pages:

  • Data entry — error on form fields, complementary to a section Alert.
  • Data — Table/DataTable that pair with Skeleton (loading) and EmptyState/ErrorState.
  • Overlays — ConfirmDialog for destructive actions that need confirmation before the Toast.
  • Layout — Grid to lay out multiple Stats; Center for a full-screen Spinner.