# Status dot

A one-glyph status indicator for online, busy, offline and error, with a distinct shape per state and a ping, turn or flash in glyphs.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the asciicn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/status-dot.json](https://asciicn.dev/r/status-dot.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://asciicn.dev/r/status-dot.json
```

## Usage [#usage]

```tsx
import { StatusDot } from "@/components/asciicn/status-dot";

export function Presence({ away }: { away: boolean }) {
  return <StatusDot status={away ? "offline" : "online"} />;
}
```

Each state has its own glyph and colour, so the shape tells them apart even without colour:

| Status    | `dot`              | `braille`           | Colour |
| --------- | ------------------ | ------------------- | ------ |
| `online`  | `●`, pings `◉ ◎`   | `⠶`, pulses `⣿`     | green  |
| `busy`    | `◐`, turns `◓ ◑ ◒` | `⣾`, orbits `⣽ ⣻ ⢿` | orange |
| `offline` | `○`, still         | `⠤`, still          | grey   |
| `error`   | `✕`, flashes `×`   | `⣿`, flashes `⠶`    | red    |

`label` replaces the state name next to the glyph, such as `"Degraded"` for a busy service. Set `showLabel={false}` to show the glyph alone; screen readers still hear the label.

## Examples [#examples]

### Service health [#service-health]

The `braille` variant in a table, with labels for each service.

```tsx title="examples/status-dot-services.tsx"
import { StatusDot } from "@/components/asciicn/status-dot";
import type { StatusDotState } from "@/components/asciicn/status-dot";

const SERVICES = [
  { latency: "84 ms", name: "api.acme.dev", status: "online" },
  { latency: "212 ms", name: "search.acme.dev", status: "busy" },
  { latency: "n/a", name: "jobs.acme.dev", status: "error" },
  { latency: "n/a", name: "legacy.acme.dev", status: "offline" },
] as const satisfies readonly {
  latency: string;
  name: string;
  status: StatusDotState;
}[];

const LABELS = {
  busy: "Degraded",
  error: "Down",
  offline: "Retired",
  online: "Operational",
} as const satisfies Record<StatusDotState, string>;

export default function StatusDotServices() {
  return (
    <table className="w-full max-w-md text-left font-mono text-xs">
      <thead className="text-muted-foreground">
        <tr>
          <th className="pb-2 font-normal">Service</th>
          <th className="pb-2 font-normal">Status</th>
          <th className="pb-2 text-right font-normal">Latency</th>
        </tr>
      </thead>
      <tbody>
        {SERVICES.map((service) => (
          <tr key={service.name} className="border-border border-t">
            <td className="py-2.5">{service.name}</td>
            <td className="py-2.5">
              <StatusDot
                status={service.status}
                label={LABELS[service.status]}
                variant="braille"
              />
            </td>
            <td className="text-muted-foreground py-2.5 text-right tabular-nums">
              {service.latency}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

### Changing state [#changing-state]

A deploy that moves through every state every 2.2 seconds, at `size="lg"`.

```tsx title="examples/status-dot-live.tsx"
"use client";

import { useEffect, useState } from "react";

import { StatusDot } from "@/components/asciicn/status-dot";
import type { StatusDotState } from "@/components/asciicn/status-dot";

// A deploy walking through its states, so each glyph and change shows.
const STAGES = [
  { label: "Building", status: "busy" },
  { label: "Health check failed", status: "error" },
  { label: "Rebuilding", status: "busy" },
  { label: "Live", status: "online" },
  { label: "Scaled to zero", status: "offline" },
] as const satisfies readonly { label: string; status: StatusDotState }[];

export default function StatusDotLive() {
  const [stage, setStage] = useState(0);
  useEffect(() => {
    const timer = window.setInterval(
      () => setStage((value) => (value + 1) % STAGES.length),
      2200
    );
    return () => window.clearInterval(timer);
  }, []);
  const current = STAGES[stage] ?? STAGES[0];

  return (
    <div className="bg-background border-border flex w-full max-w-xs items-center justify-between gap-4 rounded-lg border px-4 py-3">
      <span className="flex flex-col">
        <span className="text-sm font-medium">preview-4f2a1c9</span>
        <span className="text-muted-foreground font-mono text-xs">
          acme-web.dev
        </span>
      </span>
      <StatusDot status={current.status} label={current.label} size="lg" />
    </div>
  );
}
```

## Props [#props]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `status` | `"online" \| "busy" \| "offline" \| "error"` | `"online"` | The state to show. |
| `label` | `string` | `The state, as in "Online"` | Text next to the glyph and the name screen readers hear. |
| `showLabel` | `boolean` | `true` | Show the label. When false it is read to screen readers only. |
| `variant` | `"dot" \| "braille"` | `"dot"` | Geometric glyphs or one Braille cell per state. |
| `animate` | `boolean` | `true` | Online pings, busy turns and error flashes. |
| `color` | `AsciiColor` | `Green, orange, grey or red by state` | Glyph colour override, a palette name or any CSS colour. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Glyph cell height: 12, 16 or 20px. |
| `cell` | `number` |  | Glyph cell height in CSS pixels. Overrides size. |

## Notes [#notes]

* The root has `role="status"`, so screen readers announce a new state once, politely. The glyph is hidden from them; they hear the label.
* The animation swaps one glyph a few times a second. It pauses offscreen and in hidden tabs, and with reduced motion each state holds its first glyph.
* The glyph is DOM text in `--ascii-font`, one cell wide, so the label never shifts as it changes.