Live Context Indicator

A compact "I'm watching and updating" signal for live-data chrome. A coloured dot — optionally pulsing — paired with an inline label that shimmers each time its value mutates. Pair it with "last updated 2s ago" timestamps, active-stream chips, or any micro-region of the UI that refreshes in place.

Preview
streaming
Customize
Indicator
0
1

Installation

npx shadcn@latest add https://craftbits.dev/r/live-context-indicator.json

Usage

import { LiveContextIndicator } from "@craft-bits/core";
 
<LiveContextIndicator status="live" label="streaming" />

The label can be any ReactNode. Update its value to fire a shimmer wipe:

<LiveContextIndicator status="live" label="updated 2s ago" />
<LiveContextIndicator status="idle" label="stale 2m" />
<LiveContextIndicator status="paused" label="paused" />

Drop the label for a pure status dot — supply aria-label so it announces:

<LiveContextIndicator status="live" aria-label="Live" />

Understanding the component

  1. Status-driven palette. live paints a success-coloured dot, idle paints a warning-coloured dot, and paused paints a muted dot. The row uses text-cb-fg-muted for the label so the dot carries the signal weight.
  2. Per-status pulse defaults. live and idle pulse by default; paused does not. Pass pulse={false} to mute a pulsing status, or pulse={true} to force a normally-static status to pulse — useful for emphasising a "checking in…" beat on a paused stream.
  3. Shimmer on label change. Each time the label value changes, the inner span receives a fresh key, which restarts the gradient wipe. The wipe animates background-position masked through background-clip: text — no layout thrash, no DOM mutation beyond the key swap.
  4. Reduced motion respected. useReducedMotion() short-circuits both the pulse and the shimmer to instant fallbacks. Reduced-motion users see a static coloured dot and a plain label that swaps without animation.
  5. Live-region a11y. The root carries role="status" and aria-live="polite" so screen readers announce the label every time it updates. The dot is aria-hidden — only the label is announced.

Variants

Live (default pulse)

<LiveContextIndicator status="live" label="streaming" />

Idle (slower pulse)

<LiveContextIndicator status="idle" label="stale 2m" />

Paused (static)

<LiveContextIndicator status="paused" label="paused" />

Dot only

<LiveContextIndicator status="live" aria-label="Live" />

Props

PropTypeDefaultDescription
status'live' | 'idle' | 'paused'requiredOperational status — drives dot colour and default pulse behaviour.
labelReactNodeOptional text rendered after the dot. Shimmers each time its value changes.
pulsebooleanper-status defaultOverride the dot pulse. true forces pulsing; false forces static.
size'sm' | 'md' | 'lg''md'Row size — drives dot diameter, gap, and label scale.
classNamestringMerged onto the rendered <div> via cn().
...restHTMLAttributes<HTMLDivElement>Any other <div> attribute (aria-*, data-*, event handlers, …).

Accessibility

  • The root is announced as a live region: role="status" plus aria-live="polite". Screen readers read the label each time it updates without interrupting the user.
  • The dot is aria-hidden="true" — only the label conveys meaning to assistive tech. When omitting the label, supply aria-label so the indicator still has an accessible name.
  • Pulse and shimmer animations are both suppressed when the user has prefers-reduced-motion: reduce — the JS layer short-circuits to a static dot and a non-animated label.
  • All three status colours read from the cb semantic token system (--cb-success, --cb-warning, --cb-fg-subtle) and clear WCAG AA contrast on the default surface tokens.

Credits

  • Extracted from: algoflashcards (src/platform/ui/LiveContextIndicator.tsx). The original used the project's useTimers hook and inline bg-success / bg-warning Tailwind classes pinned to the old shadcn token system. craft-bits drops the timer hook (the shimmer only needs a key swap), tokenises every colour, switches to cb-* semantic classes, and replaces inline ease: "easeInOut" strings with EASINGS.inOut from @craft-bits/core/motion.