Skip to content

Status

System state as icon plus label plus colour — never colour alone.

Feedbackstarterstatushealthuptimestateaccessibility

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

The exact file rendered in the preview above.

import { Status } from '@/components/ui/status'
import { DemoRow, DemoStage } from './_kit'

export default function StatusDemo() {
  return (
    <DemoStage>
      <DemoRow label="Inline" description="Icon plus label — the default for detail views.">
        <Status kind="operational" label="Operational" />
        <Status kind="degraded" label="Degraded performance" />
        <Status kind="down" label="Major outage" />
        <Status kind="pending" label="Deploying" />
        <Status kind="idle" label="Not configured" />
      </DemoRow>

      <DemoRow label="Pill" description="For standalone display in a header or card.">
        <Status appearance="pill" kind="operational" label="All systems normal" />
        <Status appearance="pill" kind="degraded" label="Elevated latency" />
        <Status appearance="pill" kind="down" label="API unavailable" />
        <Status appearance="pill" kind="pending" label="Queued" />
      </DemoRow>

      <DemoRow label="Dot" description="For dense tables where an icon would crowd the row.">
        <Status appearance="dot" kind="operational" label="api.foundry.dev" />
        <Status appearance="dot" kind="degraded" label="cdn.foundry.dev" />
        <Status appearance="dot" kind="down" label="webhooks.foundry.dev" />
      </DemoRow>
    </DemoStage>
  )
}

components/demos/status.tsx

import { Status } from '@/components/ui/status'
import { DemoRow, DemoStage } from './_kit'

export default function StatusDemo() {
  return (
    <DemoStage>
      <DemoRow label="Inline" description="Icon plus label — the default for detail views.">
        <Status kind="operational" label="Operational" />
        <Status kind="degraded" label="Degraded performance" />
        <Status kind="down" label="Major outage" />
        <Status kind="pending" label="Deploying" />
        <Status kind="idle" label="Not configured" />
      </DemoRow>

      <DemoRow label="Pill" description="For standalone display in a header or card.">
        <Status appearance="pill" kind="operational" label="All systems normal" />
        <Status appearance="pill" kind="degraded" label="Elevated latency" />
        <Status appearance="pill" kind="down" label="API unavailable" />
        <Status appearance="pill" kind="pending" label="Queued" />
      </DemoRow>

      <DemoRow label="Dot" description="For dense tables where an icon would crowd the row.">
        <Status appearance="dot" kind="operational" label="api.foundry.dev" />
        <Status appearance="dot" kind="degraded" label="cdn.foundry.dev" />
        <Status appearance="dot" kind="down" label="webhooks.foundry.dev" />
      </DemoRow>
    </DemoStage>
  )
}

components/ui/status.tsx

import { CheckCircle2, AlertTriangle, XCircle, Info, CircleDashed, Clock } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Status
 *
 * Deliberately separate from Badge. A status describes the *state of a thing*
 * (a deployment, an invoice, a service) and therefore always pairs an icon
 * with a label — colour is the third signal, never the only one. That rule is
 * what makes the library legible to colour-blind users without a theme switch.
 */
export type StatusKind = 'operational' | 'degraded' | 'down' | 'pending' | 'info' | 'idle'

export interface StatusProps {
  kind: StatusKind
  label: string
  /** `dot` for dense tables, `pill` for standalone display. */
  appearance?: 'dot' | 'pill' | 'inline'
  className?: string
}

const config = {
  operational: {
    icon: CheckCircle2,
    text: 'text-success',
    bg: 'bg-success-soft border-success-line',
    dot: 'bg-success',
  },
  degraded: {
    icon: AlertTriangle,
    text: 'text-warning',
    bg: 'bg-warning-soft border-warning-line',
    dot: 'bg-warning',
  },
  down: {
    icon: XCircle,
    text: 'text-danger',
    bg: 'bg-danger-soft border-danger-line',
    dot: 'bg-danger',
  },
  pending: { icon: Clock, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
  info: { icon: Info, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
  idle: {
    icon: CircleDashed,
    text: 'text-ink-subtle',
    bg: 'bg-surface-sunken border-line',
    dot: 'bg-ink-subtle',
  },
} as const

export function Status({ kind, label, appearance = 'inline', className }: StatusProps) {
  const entry = config[kind]
  const Icon = entry.icon

  if (appearance === 'dot') {
    return (
      <span className={cn('inline-flex items-center gap-2 text-sm text-ink', className)}>
        <span className={cn('size-2 shrink-0 rounded-full', entry.dot)} aria-hidden="true" />
        {label}
      </span>
    )
  }

  if (appearance === 'pill') {
    return (
      <span
        className={cn(
          'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium',
          entry.bg,
          entry.text,
          className,
        )}
      >
        <Icon className="size-3.5 shrink-0" aria-hidden="true" />
        {label}
      </span>
    )
  }

  return (
    <span
      className={cn('inline-flex items-center gap-1.5 text-sm font-medium', entry.text, className)}
    >
      <Icon className="size-4 shrink-0" aria-hidden="true" />
      {label}
    </span>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

For the state of a thing: a service, a deployment, an invoice. Deliberately separate from Badge so that the "always pair an icon with the label" rule can be enforced by the component rather than by review.

  • Dot appearance suits dense tables; pill suits a standalone header; inline suits detail views.
  • Keep the label a real word ("Degraded performance"), not a code.
  • Pending is a state, not a failure — give it its own kind rather than reusing warning.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Inline
  • Pill
  • Dot
  • Six kinds — operational, degraded, down, pending, info, idle

Accessibility

Colour independence
Every appearance includes the label text, so the meaning survives greyscale and colour vision deficiency.
Icons
Icons are decorative; the text is the content.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.