Skip to contentVibraUI
Feedback & status

Live pulse

A status dot with a ring pulsing out of it, and the word for what it means.

A live region (role="status"), so a reader hears the switch between live and parked as it happens. The colour is never the only signal: the label says the same thing in words, and the dot itself is aria-hidden. The ring's period is four times --duration-slow, so a reader who has asked for less motion gets a still dot — the token is 0ms then — as well as under the media query, which hides the ring outright. Where LiveIndicator is a pill that says a panel is streaming, this is the dot beside a service, a region or a row.

Install

npx shadcn@latest add @vibra/live-pulse

Needs the @vibra registry in your components.json — set it up once.

Examples

Props

PropTypeDefaultDescription
labelReact.ReactNode—What the dot is reporting; it is what the live region announces, so keep it short and true.
tone"success" | "warning" | "danger" | "info" | "neutral""success"Colours the dot and its ring; neutral also quietens the label.
pulsingbooleantrueFalse parks the dot: it keeps its colour and stops ringing.

Dependencies

Registry

Source

components/ui/live-pulse.tsx
import * as React from "react"

import { cn } from "@/lib/utils"

export type LivePulseTone = "success" | "warning" | "danger" | "info" | "neutral"

// Literal class names, one per tone: Tailwind reads this file's source text, so
// a name built by interpolation would type-check and then render colourless.
const DOT: Record<LivePulseTone, string> = {
  success: "bg-success",
  warning: "bg-warning",
  danger: "bg-danger",
  info: "bg-info",
  neutral: "bg-muted-foreground",
}

const LABEL: Record<LivePulseTone, string> = {
  success: "text-foreground",
  warning: "text-foreground",
  danger: "text-foreground",
  info: "text-foreground",
  neutral: "text-muted-foreground",
}

export type LivePulseProps = React.ComponentProps<"span"> & {
  /** What the dot is reporting; it is the element's accessible name, so keep it short and true. */
  label: React.ReactNode
  tone?: LivePulseTone
  /** False parks the dot: it keeps its colour and stops ringing. */
  pulsing?: boolean
}

/**
 * A status dot with a ring pulsing out of it, and the word for what it means.
 *
 * A live region, so a reader hears the switch between live and parked as it
 * happens; the colour is never the only signal, because the label says the
 * same thing in words. The ring's period is four times `--duration-slow`,
 * which means it stops of its own accord when a reader has asked for less
 * motion — the token is 0ms then — as well as under the media query.
 */
function LivePulse({
  className,
  label,
  tone = "success",
  pulsing = true,
  ...props
}: LivePulseProps) {
  return (
    <span
      data-slot="live-pulse"
      data-tone={tone}
      data-pulsing={pulsing || undefined}
      role="status"
      className={cn("inline-flex w-fit items-center gap-2 text-xs font-medium", className)}
      {...props}
    >
      <span aria-hidden="true" className="relative flex size-2 shrink-0">
        {pulsing ? (
          <span
            data-slot="live-pulse-ring"
            className={cn(
              "absolute inline-flex size-full animate-ping rounded-full opacity-70 [animation-duration:calc(var(--duration-slow,320ms)*4)] motion-reduce:hidden in-data-[motion=reduced]:hidden",
              DOT[tone]
            )}
          />
        ) : null}
        <span
          data-slot="live-pulse-dot"
          className={cn("relative inline-flex size-full rounded-full", DOT[tone])}
        />
      </span>
      <span data-slot="live-pulse-label" className={LABEL[tone]}>
        {label}
      </span>
    </span>
  )
}

export { LivePulse }