DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Status

A status dot with two axes — presence (online, away, busy, offline; shape-coded 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.

Playground

Installation

pnpm add @tessinaui/ui

Usage

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

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 they read apart with the colour removed.

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. Presence glyphs scale with the dot; an intent glyph only appears from md, where it can be read.

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.

API Reference

Status

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

PropTypeDefaultDescription
presence"online" | "away" | "busy" | "offline"—Where someone is. Shape-coded. 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 — lucide Moon and Minus, stroked like every other icon, never filled — drawn 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.52: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 stroke weight (2.5), 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 stroked 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 aria-busy and visually hidden "Loading" text, so the announcement fires on insertion (a live region announces its content; an aria-label there would announce nothing).

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

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

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.

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

When to use which

presence or intent? Presence is about a person: where they are (online, away, busy, offline), and it is shape-coded because the four are read side by side in a list. Intent is about a thing: the state of a deployment, a sync, a connection (error, warning, success, info), one at a time, with a glyph to back the colour. Do not use presence="busy" to mean "failed" or intent="success" to mean "online". When both are given, presence wins and intent is ignored.

Status, Badge or Avatar's own dot?

  • Status — the state of a thing or the presence of a person. Meaning comes from shape and colour; there is no count and no text unless you ask for it.
  • Badge — a notification overlay: a count, a short label, "new". It says how many, not which state.
  • Avatar's presence — when the dot belongs to a person's picture, use the avatar's prop. 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.

Labels are one word in sentence case — the defaults — unless the generic word is wrong for the context; then be specific ("Sync failed", "Recording"). Localise through label; the defaults are English. Never use the enum token as copy.

Non-goals — a status dot is not a button, a toggle or a live region. It does not announce changes on its own; when a state flips and the user needs to hear it, announce that from the surrounding component.

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.

StackStepper

On this page

PlaygroundInstallationUsageExamplesDefaultPresenceIntentsSizesRoundedWith visible labelPulseOn a coloured surfaceDecorativeLocalised labelIcon visibilitySkeletonAPI ReferenceStatusSize scaleStatusSkeletonHelpersAccessibilityWhen to use whichMigrating from variant