DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ButtonIconButtonLinkLabelBadgeStatusChipAvatarShortcutSpinnerSkeletonProgressMeter
AI agentsContributing
  1. Components
  2. Status

Status

A status dot with two axes — presence (online, away, busy, offline; shape-coded from md up as disc, moon, bar and hollow ring) and intent (none, error, warning, success, info; with a glyph from md up). Named for assistive technology by a human word, optionally shown as text, with pulse, a backdrop ring for overlapping an avatar, six sizes from 6 to 20 px and a rounded scale from circle to square.

$ pnpm add @tessinaui/ui

When to use

  • The state of a thing or the presence of a person, shown as a small dot. Meaning comes from shape and colour; there is no count and no text unless you ask for it.
  • presence for a person, intent for a thing. Presence is where someone is (online, away, busy, offline) and is shape-coded because the four are read side by side in a list. Intent is the state of a deployment, a sync or a connection, one at a time, with a glyph to back the colour. Never use presence="busy" to mean "failed" or intent="success" to mean "online". When both are given, presence wins and intent is ignored.
  • Beside text that names the state, or with showLabel. A dot never carries its meaning by colour alone, and below md it has no shape to help.
  • The same state, the same word and colour everywhere. People learn a dot once; a second colour for the same state undoes that.
  • A short, specific label when the generic word is wrong. One word in sentence case ("Sync failed", "Recording"); the defaults are English, so localise through label.

When not to use

  • To show a count or a "new" marker. Use Badge, which says how many, not which state.
  • On a person's picture. Use Avatar's presence. It positions the dot on the rim, rings it in the right backdrop and composes the name ("Jane Doe, Online"). Do not absolutely-position a standalone Status over an avatar.
  • For something people press. A status dot is not a button or a toggle, and nobody will guess that it is clickable. Use Button or ToggleButton, and put the dot inside.
  • For a state nobody needs to act on or notice. Plain text says it with less noise, and a screen full of dots stops anything standing out.
  • To explain or warn at length. Use Alert; a dot names a state, it does not describe it.
  • To announce a change. Status is not a live region. When a state flips and people need to hear it, announce it from the surrounding component.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Status } from "@tessinaui/ui";
example.tsx
<Status presence="online" />
<Status presence="away" showLabel />
<Status intent="error" size="lg" label="Sync failed" pulse />

Labels are one word in sentence case, which is what the defaults are. Be specific only when the generic word is wrong for the context ("Sync failed", "Recording"), and never use the enum token as copy. Localise through label.

Examples

Default

A presence dot. With nothing set it is online, and it announces "Online" — the human word, never the enum token.

Presence

presence says where someone is. The four states are shape-coded as well as colour-coded — online is a filled disc, away a moon, busy a bar, offline a hollow ring — so from md up they read apart with the colour removed. Below md there is no room for the moon or the bar, so a small dot needs a visible label (showLabel) or text beside it that names the state.

Intents

intent says what state something is in: a plain filled dot with a glyph from md up. none is the neutral dot, painted in --muted-foreground so it still clears 3:1 against the page.

Sizes

Six diameters — 2xs 6 px, xs 8 px, sm 10 px, md 12 px, lg 16 px, xl 20 px. Glyphs, presence and intent alike, appear from md up, where they can be read, and scale with the dot. Below md the dot carries its colour alone; only offline keeps its shape, the hollow ring.

Rounded

The dot's radius derives from rounded: full (the default) is a circle, none a square marker, and sm to xl are rounded squares. The presence glyphs are drawn for the circle; square markers read best with intents.

With visible label

showLabel renders the name as text beside the dot. The visible text is the accessible name, so the root drops role="img" and aria-label — nothing is read twice. label replaces the default word when the generic one is not specific enough.

Pulse

pulse is Tailwind's own animate-pulse — the same two-second opacity curve every reference implementation hand-writes — and it is switched off under prefers-reduced-motion. Reserve it for a state that needs attention now.

On a coloured surface

Nothing here sets tone. The <Surface> publishes its fill as --surface-fill, and that is the backdrop the ring and the offline hollow are painted in — they follow the primary fill instead of assuming the page is white. The glyphs do NOT follow it: the moon and the bar are drawn in their fill's own locked ink (--on-warning, --on-error), so they stay legible on every plate. The label inherits the surface ink.

Decorative

label={null} makes the dot decorative — no role, hidden from assistive technology. Use it only when the meaning is already in the text of what the dot sits beside, as in these rows, which each read "Jane Doe … Online" on their own. An Avatar does this for you: its dot is decorative and the avatar carries "Jane Doe, Online".

Localised label

The default names are English words. Pass label to localise — it is the accessible name either way, and the visible text with showLabel.

Icon visibility

Intents draw their glyph from md up. showIcon={false} suppresses it; it cannot force a glyph below md, where an 8 px mark would not be legible.

Skeleton

StatusSkeleton reads its diameter from the same STATUS_SIZE map as the real dot and announces "Loading" as a live region.

RTL

Status has no dir prop. It follows the direction it sits in, so with showLabel the dot moves to the right of the text. The presence glyphs do not flip. Pass label, since the default names are English.

API Reference

Status

Extends the native <span> attributes (children excluded).

PropTypeDefaultDescription
presence"online" | "away" | "busy" | "offline"—Where someone is. Shape-coded from md up. Wins over intent when both are given. With neither set the dot is online
intent"none" | "error" | "warning" | "success" | "info"—What state something is in. A filled dot with a glyph from md up. Ignored when presence is set
variant (deprecated)"online" | "away" | "busy" | "offline" | "neutral" | "error" | "warning" | "success" | "info"—Since 2026-08-23 — the old single enum, mapped onto presence / intent (see Migrating from variant). An explicit presence or intent wins over it. Removed at the next major
size"2xs" | "xs" | "sm" | "md" | "lg" | "xl""md"Dot diameter: 6 / 8 / 10 / 12 / 16 / 20 px
labelstring | nullthe English word for the stateAccessible name — and the visible text with showLabel. Pass a translation to localise. null makes the dot decorative: no role, aria-hidden
showLabelbooleanfalseRender the label as visible text beside the dot. The text is then the name; no role or aria-label is set
pulsebooleanfalsePulse the dot (animate-pulse, off under prefers-reduced-motion)
ringbooleanfalseDraw a cut-out ring in the backdrop colour — the nearest <Surface>'s fill, or the page — for a dot that overlaps something. Width steps with size
showIconbooleantrue for intentsShow the intent glyph inside the dot. Only suppresses: below md there is never a glyph
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Corner treatment of the dot: full is a circle, none a square marker, the steps between are rounded squares
tone"default" | "on-color"inherited from <Surface>On-color, a visible label takes currentColor — the surface ink — instead of --foreground

Default labels — Online, Away, Busy, Offline for presence; Status, Error, Warning, Success, Info for intents.

Data attributes — the root carries data-slot="status", either data-presence or data-intent (whichever axis is in effect), data-size and data-tone; the inner dot carries data-slot="status-dot".

Size scale

SizeDiameterBackdrop ringoffline outlineGlyph (presence + intent)
2xs6 px1 px1 px— (colour alone)
xs8 px1 px1 px—
sm10 px1 px1 px—
md12 px1.5 px1.25 px8 px
lg16 px2 px1.75 px10 px
xl20 px2 px2 px12 px

The presence glyphs are the library's own icons, a moon and a bar, drawn at the bold weight in the fill's locked ink, exactly as the intent glyphs are: the moon is --on-warning (black in both themes, 9.67:1 / 12.36:1) and the bar --on-error (4.77:1 / 5.44:1). They were briefly painted in the backdrop colour instead, which put a white moon on amber at ~1.9:1 in light — a glyph must contrast with its own dot, not with the page behind it. Presence and intent share one size and one weight, so a moon and a check sitting in the same row read as one icon set. Both appear from md up: below 12 px a glyph is a smudge, not a shape.

online is a plain disc and offline a hollow ring at every size. The ring is not an icon — it is the dot's own boundary, drawn as a border so it follows the radius cascade (a squared-off dot gets a squared-off ring). Its width comes from the same stroke constant, mapped onto the dot's box instead of a glyph's, so the four states carry one outline weight; it is floored at 1 px, below which a border fades to a subpixel hairline and stops carrying the state.

StatusSkeleton

Extends the native <div> attributes.

PropTypeDefaultDescription
size"2xs" | "xs" | "sm" | "md" | "lg" | "xl""md"Matches the diameter of the dot it stands in for
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Matches the radius of the dot it stands in for

Renders role="status" with visually hidden "Loading" text, written 150ms after mount so the announcement fires on insertion (a live region announces its content; an aria-label there would announce nothing, and aria-busy would silence it).

Helpers

ExportTypeDescription
STATUS_SIZERecord<StatusSize, string>The diameter utility per size (size-1.5 … size-5). The skeleton and Avatar read it
STATUS_PXRecord<StatusSize, number>The same diameters as numbers (6 · 8 · 10 · 12 · 16 · 20), for consumers that place the dot with calc() — Avatar does
STATUS_RINGRecord<StatusSize, string>The ring width utility per size (ring-1 … ring-2)
resolveStatusAxes(input)({ presence?, intent?, variant? }) => { presence?, intent? }Resolves the two axes from props: presence first, then intent, then the deprecated variant alias. Shared with Avatar and Chip
statusLabelFor(state)(state: StatusPresence | StatusIntent) => stringThe default accessible name for a state — for a component that composes it into its own name, the way Avatar builds "Jane Doe, Online"

Types: StatusPresence, StatusIntent, StatusSize, StatusRounded, StatusTone, StatusProps, StatusSkeletonProps and the deprecated StatusVariant.

Accessibility

No APG pattern applies — a status dot is an image, not a widget. It is never focusable and has no keyboard interaction; if a state needs an action, put the dot inside the control and let the control carry the name.

What is announced

VoiceOver + Safari passed 2026-09-14 on the first row: <Status presence="online" /> read "Online image". The other rows are expected from the markup, not a recorded run.

MarkupVoiceOver
<Status presence="online" />"Online, image" — role="img" named by label
<Status intent="error" label="Sync failed" />"Sync failed, image"
<Status presence="away" showLabel />"Away" — plain text; no role, no aria-label, so it is not read twice
<Status presence="busy" label={null} />Nothing — aria-hidden, no role
<Avatar name="Jane Doe" presence="online" />"Jane Doe, Online, image" — the avatar composes the name; its dot is decorative
<StatusSkeleton />"Loading" on insertion — role="status" with real (visually hidden) text

The intent and presence glyphs are aria-hidden; the name comes from the word, never from the icon.

Why shapes

From md up, colour is never the only signal (WCAG 1.4.1). The amber away dot is 2.17:1 against white — under the 3:1 a non-text indicator needs — and to a colour-blind user the green, amber and red fills collapse into one; dark mode shifts the tints again. So presence is drawn: a filled disc, a moon, a bar, a hollow ring. The glyphs are painted in the fill's own locked ink, so the shape holds its contrast on the dot itself; the ring and the offline hollow take the backdrop colour (--surface-fill, or the page), which is what seats the dot on whatever it overlaps. Below md there is no room for the moon or the bar and only offline keeps its shape, so a small dot must sit beside text that names its state, or carry showLabel.

Contrast

  • The neutral dot (intent="none") is --muted-foreground, not --secondary: neutral-200 on white is 1.2:1 and invisible, where neutral-500 clears the 3:1 boundary contrast a non-text indicator needs (WCAG 1.4.11).
  • offline paints the backdrop and carries its colour in a --outline-border ring, so the hollow stays hollow on the page and on a coloured surface.

When to use label={null}

Only when the dot's meaning is already in the accessible name of the thing it decorates. A list row whose text says "Online", a chip whose label carries the state, an Avatar (which composes "Jane Doe, Online" itself). A lone decorative dot tells a screen-reader user nothing — if there is no text nearby that says what it means, keep the default name or set showLabel.

Motion — pulse is disabled under prefers-reduced-motion (WCAG 2.3.3).

Migrating from variant

variant was one enum that mixed where someone is with what state something is in. It still compiles for one minor and maps as below; move each call site to the axis it meant.

variantNow
"online"presence="online"
"away"presence="away"
"busy"presence="busy"
"offline"presence="offline"
"neutral"intent="none" — the grey dot. If it meant "not here", use presence="offline" (the hollow ring) instead
"error"intent="error"
"warning"intent="warning"
"success"intent="success"
"info"intent="info"

The size scale also moved: it now starts at 2xs (6 px) and tops out at xl (20 px). The old xs / sm / md / lg (8 / 12 / 16 / 20 px) are today's xs / md / lg / xl; the old 24 px xl has no equivalent.

BadgeChip

On this page

When to useWhen not to useInstallationUsageExamplesDefaultPresenceIntentsSizesRoundedWith visible labelPulseOn a coloured surfaceDecorativeLocalised labelIcon visibilitySkeletonRTLAPI ReferenceStatusSize scaleStatusSkeletonHelpersAccessibilityWhat is announcedWhy shapesContrastWhen to use label={null}Migrating from variant