Components · Feedback indicators

Indicator

A 6 px dot. Marks new content, completion state, or live presence — anywhere a single pixel of metadata earns its place beside text or an icon. Smaller than a badge, quieter than a banner, more permanent than a toast.

Documentedby Derek Fidler

Default

Overview

The Indicator is the smallest piece of UI in the system — a 6 px coloured dot that signals state. It exists for the moments where a word is too much and a number is too specific: there’s something new here, this is online, this stage is complete. It does one job, has one shape, and gets out of the way.

Smallest does the most

One dot can change the meaning of an entire screen. A neutral dot on a row label says “there’s new content here” without an unread counter, a banner, or a toast. Reach for it when you want presence without noise — and pair it with text whenever the meaning is non-obvious.

Tones

Seven tones. The colour mapping mirrors Badge so a status dot reads consistently whether it appears alone or inside a pill.

  • Neutral
    tone="neutral"

    New / unread / default attention. Charcoal on light, inverse on dark.

  • Success
    tone="success"

    Online, complete, healthy. Pair with text — operators don’t read green by feel.

  • Warning
    tone="warning"

    Needs review or about to expire. Slow signal, not urgent.

  • Danger
    tone="danger"

    Failed, offline, blocking. Reserve it — every red dot earns a reason.

  • Info
    tone="info"

    Update available, change recorded. Quieter than neutral.

  • Discovery
    tone="discovery"

    AI / experimental / model-related state.

Sizes

Three sizes. md at 6 px is the default and matches the Figma. Reach for sm only when the dot sits inside dense table metadata; reach for lg only when it stands alone with no text companion.

  • size="sm"4 px

    Inline beside dense, list-row metadata.

  • size="md"6 px

    Default — beside section headings, in nav.

  • size="lg"8 px

    Standalone signals at the edge of a card.

Filled vs outline

Two variants. Filled is the loud one — “there is something here.” Outline is the quiet one — “this slot is accounted for, but at rest.” They’re designed as a pair: read/unread, done/pending, present/absent.

  • Stripe webhook signature mismatch
  • Adyen webhook delivered
  • Daily settlement reconciled
  • Risk model retraining queued
Filled = unread, outline = read. Same hue, different state.
  • Connect repo
  • Add manifest
  • Pick a model
  • Invite reviewers
  • Promote to demo-ready
Outline = pending, filled success = done. Onboarding checklist.

Pulse

Set pulse to mark a livestate — recording, streaming, transcribing. It’s the loudest thing the indicator can do, so use it sparingly: a pulse on every dot is just visual chatter.

Live transcribing
Recording
Streaming events

Reduced motion

The pulse uses Tailwind’s motion-safe:animate-ping — when a user has prefers-reduced-motion on, the dot stops pulsing and stays solid. The state remains legible without the animation.

Pairings

Three places it shows up: pinned to an icon’s corner, inline beside a nav label, and inside a tab to telegraph the environment’s health. The component itself is just a dot — positioning is the parent’s job.

Pinned to an icon’s top-right corner. The pulse on the left marks the unread state — the right is at rest.
Beside a nav label — a quiet info dot tells the user the section has new content without a number.
Tab labels carry an environment’s health at a glance. Local has no dot — there’s nothing to report.

On dark surfaces

On a charcoal panel, the neutral charcoal dot disappears. Use tone="inverse" for neutral on dark; saturated tones (success / danger / info) work on either surface unchanged.

New release notes
Connected
On a dark surface — tone=“inverse” for neutral, or any saturated tone for status.
New release notes
Same content on a light surface — neutral is the default.

Anatomy

Two parts. The dot is everything; the optional pulse ring sits behind it.

  1. Dot

    6 px circle in the tone’s saturated foreground colour (e.g. #005734 for success). The same hue used by Badge's text foreground — so dot and pill read as the same state.

  2. Pulse ring

    Optional. When pulseis on, a 6 px ring of the same hue expands and fades behind the dot using Tailwind’s animate-ping. Suppressed under reduced-motion.

Accessibility

  • Standalone dots need a label.If the indicator isn’t paired with text, pass an aria-label. The component then renders with role="status" so a screen reader can announce the state.
  • When paired with text, hide the dot. Omit aria-label — the component falls back to aria-hidden="true" so the same state isn’t announced twice.
  • Colour is never the only signal.Don’t mark “done” with green only — pair the colour with position (filled vs outline) or text. Operators with red-green colour vision deficiency need to read the meaning, not the hue.
  • Pulse respects user preferences. prefers-reduced-motion: reduce stops the pulse — the static dot still communicates the state, so nothing is lost.

Code

tsx

import { Indicator } from "@flatpay-dk/ui";

// Inline beside a label — paired with text, no aria-label needed
<span className="flex items-center gap-2">
  <Indicator tone="success" />
  Demo-ready
</span>

// Standalone — must announce its meaning
<Indicator tone="danger" aria-label="Webhook failed" />

// Pinned to an icon's corner with pulse for live state
<button className="relative" aria-label="Inbox, 3 unread">
  <MailIcon />
  <span className="absolute right-1 top-1">
    <Indicator tone="danger" pulse />
  </span>
</button>

// Read / unread — same hue, different variant
<Indicator tone="info" variant={message.read ? "outline" : "filled"} />

Best practices

Demo-ready

Do

Pair the dot with text whenever the meaning isn't obvious from context. Operators don't read green by feel.

Item one Item two Item three

Don't

Don't sprinkle colours across a list to decorate it. The dot earns its hue by communicating state.

Recording

Do

Use pulse only for live, momentary states — recording, streaming, transcribing. Stop it when the activity stops.

Always pulsing

Don't

Don't pulse decoratively. A pulse that runs forever on every page is the kind of motion that gets turned off in settings.

Props

PropTypeDefaultDescription
tone"neutral" | "success" | "warning" | "danger" | "info" | "discovery" | "inverse""neutral"The state the dot communicates. Hue mapping mirrors Badge.
variant"filled" | "outline""filled"Filled = present / unread / done. Outline = pending / read / inactive. Use as a pair.
size"sm" | "md" | "lg""md"4 / 6 / 8 px. Default is 6 px, matching the Figma.
pulsebooleanfalseWhen true, paints a softly expanding ring beneath the dot. Reserved for live, momentary states.
aria-labelstringRequired when standalone. Omit when paired with descriptive text — the dot falls back to aria-hidden.
classNamestringPass-through class for positioning (e.g. absolute placement on an icon).