Display
Status dot
A small coloured dot with a status in words, for service health, presence and live states.
- Built on
<span>CSS animations- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 1.2 kB
- Dependencies
- React
- APIOperational
- WebhooksDegraded
- BillingDown
Try its options
Loading its options.
Code
import { StatusDot } from "@/components/equip/status-dot";
const services = [
{ name: "API", status: "Operational", tone: "ok" },
{ name: "Webhooks", status: "Degraded", tone: "warn" },
{ name: "Billing", status: "Down", tone: "danger" },
] as const;
export default function Demo() {
return (
<ul style={{ display: "grid", gap: 10, width: 180, margin: 0, padding: 0, listStyle: "none", fontSize: 13 }}>
{services.map((s) => (
<li key={s.name} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<span style={{ fontWeight: 650 }}>{s.name}</span>
<StatusDot tone={s.tone} label={s.status} />
</li>
))}
</ul>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<ul style="display:grid;gap:10px;width:180px;margin:0;padding:0;list-style:none;font-size:13px">
<li style="display:flex;align-items:center;justify-content:space-between;gap:8px">
<span style="font-weight:650">API</span>
<span class="eq-status-dot" data-tone="ok">
<span class="eq-status-dot-mark" aria-hidden="true"></span>
<span class="eq-status-dot-label">Operational</span>
</span>
</li>
<li style="display:flex;align-items:center;justify-content:space-between;gap:8px">
<span style="font-weight:650">Webhooks</span>
<span class="eq-status-dot" data-tone="warn">
<span class="eq-status-dot-mark" aria-hidden="true"></span>
<span class="eq-status-dot-label">Degraded</span>
</span>
</li>
<li style="display:flex;align-items:center;justify-content:space-between;gap:8px">
<span style="font-weight:650">Billing</span>
<span class="eq-status-dot" data-tone="danger">
<span class="eq-status-dot-mark" aria-hidden="true"></span>
<span class="eq-status-dot-label">Down</span>
</span>
</li>
</ul>Examples
Tones
Neutral is drawn as a hollow ring, so paused never looks like a faded version of healthy.
PausedHealthyRate limitedOffline
Code
import { StatusDot } from "@/components/equip/status-dot";
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 16 }}>
<StatusDot label="Paused" />
<StatusDot tone="ok" label="Healthy" />
<StatusDot tone="warn" label="Rate limited" />
<StatusDot tone="danger" label="Offline" />
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-wrap:wrap;gap:16px">
<span class="eq-status-dot" data-tone="neutral">
<span class="eq-status-dot-mark" aria-hidden="true"></span>
<span class="eq-status-dot-label">Paused</span>
</span>
<span class="eq-status-dot" data-tone="ok">
<span class="eq-status-dot-mark" aria-hidden="true"></span>
<span class="eq-status-dot-label">Healthy</span>
</span>
<span class="eq-status-dot" data-tone="warn">
<span class="eq-status-dot-mark" aria-hidden="true"></span>
<span class="eq-status-dot-label">Rate limited</span>
</span>
<span class="eq-status-dot" data-tone="danger">
<span class="eq-status-dot-mark" aria-hidden="true"></span>
<span class="eq-status-dot-label">Offline</span>
</span>
</div>Live and in progress
Keep pulse for something happening right now; with reduced motion the ring stays hidden and the dot is still.
LiveDeploying to eu-west-1Deployed 4 min ago
Code
import { StatusDot } from "@/components/equip/status-dot";
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 16 }}>
<StatusDot tone="danger" pulse label="Live" />
<StatusDot tone="warn" pulse label="Deploying to eu-west-1" />
<StatusDot tone="ok" label="Deployed 4 min ago" />
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-wrap:wrap;gap:16px">
<span class="eq-status-dot" data-tone="danger" data-pulse="true">
<span class="eq-status-dot-mark" aria-hidden="true"><span class="eq-status-dot-ring"></span></span>
<span class="eq-status-dot-label">Live</span>
</span>
<span class="eq-status-dot" data-tone="warn" data-pulse="true">
<span class="eq-status-dot-mark" aria-hidden="true"><span class="eq-status-dot-ring"></span></span>
<span class="eq-status-dot-label">Deploying to eu-west-1</span>
</span>
<span class="eq-status-dot" data-tone="ok">
<span class="eq-status-dot-mark" aria-hidden="true"></span>
<span class="eq-status-dot-label">Deployed 4 min ago</span>
</span>
</div>Hidden label
Beside an avatar or a name the dot can stand alone; the hidden label still tells screen readers the status.
- OnlineMaya Okafor
- AwayDaniel Osei
- OfflineLena Vogel
Code
import { StatusDot } from "@/components/equip/status-dot";
const people = [
{ name: "Maya Okafor", tone: "ok", status: "Online" },
{ name: "Daniel Osei", tone: "warn", status: "Away" },
{ name: "Lena Vogel", tone: "neutral", status: "Offline" },
] as const;
export default function Example() {
return (
<ul style={{ display: "grid", gap: 8, margin: 0, padding: 0, listStyle: "none", fontSize: 14 }}>
{people.map((p) => (
<li key={p.name} style={{ display: "flex", alignItems: "center", gap: 8 }}>
<StatusDot tone={p.tone} label={p.status} hideLabel />
{p.name}
</li>
))}
</ul>
);
}HTMLworks in any framework, with the part's CSS
<ul style="display:grid;gap:8px;margin:0;padding:0;list-style:none;font-size:14px">
<li style="display:flex;align-items:center;gap:8px"><span class="eq-status-dot" data-tone="ok" data-hide-label="true"><span class="eq-status-dot-mark" aria-hidden="true"></span><span class="eq-status-dot-label">Online</span></span>Maya Okafor</li>
<li style="display:flex;align-items:center;gap:8px"><span class="eq-status-dot" data-tone="warn" data-hide-label="true"><span class="eq-status-dot-mark" aria-hidden="true"></span><span class="eq-status-dot-label">Away</span></span>Daniel Osei</li>
<li style="display:flex;align-items:center;gap:8px"><span class="eq-status-dot" data-tone="neutral" data-hide-label="true"><span class="eq-status-dot-mark" aria-hidden="true"></span><span class="eq-status-dot-label">Offline</span></span>Lena Vogel</li>
</ul>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
label | ReactNode | none | The status in words. Required, even when hidden. |
tone | "neutral" | "ok" | "warn" | "danger" | "neutral" | What the status means. Neutral draws a hollow ring. |
hideLabel | boolean | false | Hides the label visually; screen readers still read it. |
pulse | boolean | false | A ring that grows from the dot and fades. Only for something live or in progress. |
...rest | ComponentProps<"span"> | none | Every native span attribute. |
Anatomy
| root | .eq-status-dot | A span. |
|---|---|---|
| mark | .eq-status-dot-mark | The 8px dot. Hidden from assistive tech. |
| ring | .eq-status-dot-ring | Only with pulse. Rests invisible, so with motion off the dot is still. |
| label | .eq-status-dot-label | Visually hidden with hideLabel. |
Notes
- Colour alone does not reach everyone, so the label is required and must name the status: Down, not just a red dot.
- Plain text with no role; if the status changes while the page is open, put it inside a role="status" region so the change is announced.
- The pulse runs for four times --eq-dur-move, so it stops when the motion tokens go to zero under reduced motion.
- Use pulse for one thing that is happening now, such as Live or Deploying; a page full of pulsing dots says nothing.
- Renders on the server with no client JavaScript.
- Distinct from badge, which frames the status in a filled label; a status dot is quieter and sits in running text or table cells.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/status-dot.tsx37 lines
import type { ComponentProps, ReactNode } from "react";
import "./status-dot.css";
export type StatusDotProps = ComponentProps<"span"> & {
/** What the status means. The label has to say it too. */
tone?: "neutral" | "ok" | "warn" | "danger";
/** The status in words. Always required, even when hidden. */
label: ReactNode;
/** Hides the label visually. It is still read by screen readers. */
hideLabel?: boolean;
/** A ring that grows from the dot and fades, for a live or in-progress state only. */
pulse?: boolean;
};
export function StatusDot({
tone = "neutral",
label,
hideLabel = false,
pulse = false,
className,
...rest
}: StatusDotProps) {
return (
<span
{...rest}
className={["eq-status-dot", className].filter(Boolean).join(" ")}
data-tone={tone}
data-pulse={pulse || undefined}
data-hide-label={hideLabel || undefined}
>
<span className="eq-status-dot-mark" aria-hidden="true">
{pulse && <span className="eq-status-dot-ring" />}
</span>
<span className="eq-status-dot-label">{label}</span>
</span>
);
}components/equip/status-dot.css82 lines
@layer equip {
.eq-status-dot {
--_tone: var(--eq-ink-2);
display: inline-flex;
align-items: center;
gap: var(--eq-space-2);
min-width: 0;
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-s) / 1.25 var(--eq-font);
vertical-align: middle;
}
.eq-status-dot[data-tone="ok"] {
--_tone: var(--eq-ok);
}
.eq-status-dot[data-tone="warn"] {
--_tone: var(--eq-warn);
}
.eq-status-dot[data-tone="danger"] {
--_tone: var(--eq-danger);
}
.eq-status-dot-mark {
position: relative;
flex: none;
width: 8px;
height: 8px;
border-radius: var(--eq-radius-full);
background: var(--_tone);
}
/* Neutral is a hollow ring, so it never reads as a dimmed version of a coloured state. */
.eq-status-dot[data-tone="neutral"] .eq-status-dot-mark {
box-sizing: border-box;
border: 1.5px solid var(--_tone);
background: transparent;
}
/* The ring rests invisible, so with motion off (a zero duration ends the animation at once)
the dot is simply still. The loop is tied to the motion tokens and stops with them. */
.eq-status-dot-ring {
position: absolute;
inset: 0;
border-radius: inherit;
background: var(--_tone);
opacity: 0;
pointer-events: none;
animation: eq-status-dot-pulse calc(var(--eq-dur-move) * 4) var(--eq-ease) infinite;
}
@keyframes eq-status-dot-pulse {
from {
opacity: 0.45;
scale: 1;
}
to {
opacity: 0;
scale: 2.75;
}
}
.eq-status-dot-label {
min-width: 0;
overflow-wrap: anywhere;
}
.eq-status-dot[data-hide-label] .eq-status-dot-label {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
}