Skip to content
caliper ui docs

Components

Status icon

The small status glyph beside a task: a dashed ring for backlog, an open ring for todo, a ring filling like a pie while in progress and in review, a filled disc with a tick when done and a crossed disc when cancelled. Every state is built from the same four shapes, so a change animates from one to the next.

Free
pnpm dlx shadcn@latest add @caliper/status-icon

Cycle 14

Press a status to move the issue along.

  • CAL-212Billing page shows old plan name
  • CAL-208Export usage as CSV
  • CAL-199Invite flow loses the role on retry
  • CAL-187Dark mode chart gridlines
  • CAL-176Weekly digest email
  • CAL-160Legacy webhooks v1
Installs components/ui/status-icon.tsxDepends on none beyond ReactMotion shared motion tokens

When to use

Use it for

  • Issue trackers, task lists, kanban cards and release checklists.
  • Anywhere status sits beside a title and must read at 16px.

Skip it when

  • A status that needs words to be understood the first time; pair it with a label or a tooltip.
  • More than these six states; it is a shape language, not a palette.

Decisions

Why it is built this way, so you know what to keep when you change your copy.

Behaviour

One set of shapes
Ring, pie, disc and mark are always there; a status sets their dashes, fill and scale, so a change is a transition instead of a swap.
Named
It is an image named by its status ("In progress"); pass label="" when the row already says it.

Styling

Shape first, then colour
States differ by shape and fill, so they read for people who cannot tell the colours apart; the hue (amber working, blue in review, green done, grey waiting) only confirms it and eases across with the shape.

Installation

terminal
pnpm dlx shadcn@latest add @caliper/status-icon

Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage

This is the code behind the preview above.

example.tsx
import { useState, type ReactNode } from "react"
import { StatusIcon, type IssueStatus } from "@/components/ui/status-icon"

function Panel({ title, context, className = "max-w-md", children }: { title: string; context: string; className?: string; children: ReactNode }) {
  return (
    <section className={`grid w-full gap-5 rounded-overlay border border-border bg-background p-6 shadow-resting max-sm:p-4 ${className}`}>
      <div className="grid gap-1">
        <p className="font-medium">{title}</p>
        <p className="text-sm text-muted-foreground">{context}</p>
      </div>
      {children}
    </section>
  );
}

const ORDER: IssueStatus[] = ["backlog", "todo", "in-progress", "in-review", "done", "cancelled"];

export function StatusIconDemo() {
  const [issues, setIssues] = useState([
    { id: "CAL-212", title: "Billing page shows old plan name", status: "in-progress" as IssueStatus, progress: 0.4 },
    { id: "CAL-208", title: "Export usage as CSV", status: "in-review" as IssueStatus, progress: 0.75 },
    { id: "CAL-199", title: "Invite flow loses the role on retry", status: "todo" as IssueStatus, progress: 0.5 },
    { id: "CAL-187", title: "Dark mode chart gridlines", status: "done" as IssueStatus, progress: 1 },
    { id: "CAL-176", title: "Weekly digest email", status: "backlog" as IssueStatus, progress: 0.5 },
    { id: "CAL-160", title: "Legacy webhooks v1", status: "cancelled" as IssueStatus, progress: 0.5 },
  ]);
  return (
    <Panel title="Cycle 14" context="Press a status to move the issue along." className="max-w-lg">
      <ul className="grid">
        {issues.map((issue, index) => (
          <li key={issue.id} className="flex items-center gap-3 border-b border-border py-2.5 text-sm last:border-0">
            <button
              type="button"
              className="grid size-8 place-items-center rounded-full outline-none hover:bg-muted focus-visible:outline-2 focus-visible:outline-ring"
              aria-label={`Change status of ${issue.id}`}
              onClick={() =>
                setIssues((list) =>
                  list.map((item, i) => (i === index ? { ...item, status: ORDER[(ORDER.indexOf(item.status) + 1) % ORDER.length]! } : item)),
                )
              }
            >
              <StatusIcon status={issue.status} progress={issue.progress} />
            </button>
            <span className="w-16 font-mono text-xs text-muted-foreground">{issue.id}</span>
            <span className="min-w-0 flex-1 truncate">{issue.title}</span>
          </li>
        ))}
      </ul>
    </Panel>
  );
}

API reference

StatusIcon

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
status *IssueStatus—
progressnumber0.5How far an in-progress item is, 0 to 1. In review always shows three quarters.
sizenumber16Size in px.
labelstring—Read instead of the status name. Pass an empty string when a visible label already says it.

Accessibility

It is built on native elements, so roles and focus come from the browser.

No custom key handling; Tab and the native activation keys do the work.

Motion

Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.

All Changing values examples