Indicator
Two related things. .indicator is a status dot that sits in the flow of
text. .with-indicator wraps something — an icon button, an avatar — and
positions an .indicator-badge over its corner. Neither says anything to a
screen reader, which is the most important sentence on this page.
The status dot
A small circle in one of four tones, sized to sit beside text. Use it where a
.badge would be too heavy — a row of services
with an up-or-down state, a presence marker beside a name.
API — operational
Exports — degraded
Webhooks — down
Beta — enrolled
<div class="stack stack-2">
<p class="cluster cluster-tight"><span class="indicator indicator-good"></span> API — operational</p>
<p class="cluster cluster-tight"><span class="indicator indicator-warn"></span> Exports — degraded</p>
<p class="cluster cluster-tight"><span class="indicator indicator-bad"></span> Webhooks — down</p>
<p class="cluster cluster-tight"><span class="indicator indicator-brand"></span> Beta — enrolled</p>
</div>
<div class="stack stack-2">
<p class="cluster cluster-tight"><span class="indicator indicator-good"></span> API — operational</p>
<p class="cluster cluster-tight"><span class="indicator indicator-warn"></span> Exports — degraded</p>
<p class="cluster cluster-tight"><span class="indicator indicator-bad"></span> Webhooks — down</p>
<p class="cluster cluster-tight"><span class="indicator indicator-brand"></span> Beta — enrolled</p>
</div>
.indicator-lg makes it 11px, and .indicator-ring adds a
two-pixel ring in the surface colour — which is what keeps a dot legible when it sits
over an image or another colour.
The corner badge
.with-indicator is the positioning wrapper: position:
relative and display: inline-flex, so whatever it contains keeps
its own size and the badge can be placed against it.
Inside it, .indicator-badge is a count,
.indicator-badge-dot is a dot with no number, and
.indicator-badge-brand is the brand-filled variant.
<span class="with-indicator">
<button class="btn btn-icon btn-ghost" aria-label="Notifications, 3 unread">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#bell"></use></svg>
</button>
<span class="indicator-badge" aria-hidden="true">3</span>
</span>
<span class="with-indicator">
<button class="btn btn-icon btn-ghost" aria-label="Messages, unread">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#mail"></use></svg>
</button>
<span class="indicator-badge indicator-badge-dot" aria-hidden="true"></span>
</span>
<span class="with-indicator">
<span class="avatar">AC</span>
<span class="indicator-badge indicator-badge-dot" aria-hidden="true"></span>
</span>
<span class="with-indicator">
<button class="btn btn-icon btn-ghost" aria-label="Notifications, 3 unread">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#bell"></use></svg>
</button>
<span class="indicator-badge" aria-hidden="true">3</span>
</span>
<span class="with-indicator">
<button class="btn btn-icon btn-ghost" aria-label="Messages, unread">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#mail"></use></svg>
</button>
<span class="indicator-badge indicator-badge-dot" aria-hidden="true"></span>
</span>
<span class="with-indicator">
<span class="avatar">AC</span>
<span class="indicator-badge indicator-badge-dot" aria-hidden="true"></span>
</span>
Look at what the examples do: the number is in the button's
aria-label — "Notifications, 3 unread" — and the visible badge is
aria-hidden. That is the pattern, and the reason for it is in
Accessibility.
Classes
Generated from src/22-nav.css by tools/docs/extract.mjs.
| Class | Layer | What it does | Source |
|---|---|---|---|
.indicator |
deck.components |
Status dots, counts on an icon, and unread markers. | 22-nav.css:724 |
.indicator-bad |
deck.components |
Sets background. | 22-nav.css:734 |
.indicator-badge |
deck.components |
No declarations. | 22-nav.css:741 |
.indicator-badge-brand |
deck.components |
No declarations. | 22-nav.css:766 |
.indicator-badge-dot |
deck.components |
No declarations. | 22-nav.css:758 |
.indicator-brand |
deck.components |
Sets background. | 22-nav.css:735 |
.indicator-good |
deck.components |
Sets background. | 22-nav.css:732 |
.indicator-lg |
deck.components |
Sets inline-size, block-size. | 22-nav.css:736 |
.indicator-ring |
deck.components |
Sets box-shadow. | 22-nav.css:737 |
.indicator-warn |
deck.components |
Sets background. | 22-nav.css:733 |
.with-indicator |
deck.components |
A count or dot anchored to a corner of something | 22-nav.css:740 |
Tokens
| Token | Live | Declared value |
|---|---|---|
--good-500 |
oklch(62% var(--chroma-status) var(--hue-good)) |
|
--warn-500 |
oklch(76% var(--chroma-status) var(--hue-warn)) |
|
--bad-500 |
oklch(58% calc(var(--chroma-status) * 1.25) var(--hue-bad)) |
|
--brand-500 |
oklch(60.8% var(--chroma-brand) var(--hue-brand)) |
|
--brand-600 |
oklch(51.6% var(--chroma-brand) var(--hue-brand)) |
|
--text-on-brand |
oklch(99% .01 var(--hue-brand)) |
|
--surface |
light-dark(var(--ink-0), var(--ink-950)) |
|
--r-full |
999px |
Accessibility
-
Both are invisible to a screen reader. A dot is an empty
<span>with a background; a badge is a number floating beside a control. Neither is part of any accessible name unless you put it there. -
Put the count in the control's label.
aria-label="Notifications, 3 unread"on the button, andaria-hidden="true"on the badge. One thing to announce, and it stays correct because the label is what you update. -
A count that changes should be announced. Updating the label alone
is silent. If new notifications arrive while the reader is on the page, that needs an
aria-liveregion — the badge cannot do it. - Colour is the entire signal on a dot. Green, amber and red are the only difference between the four tones, and they are indistinguishable to some readers. The examples pair every dot with a word for exactly that reason.
- The badge overlaps its control. It sits on the corner of the button, so it can cover part of the target and — for a two- or three-digit count — extend past it. Check that a large number does not push outside a bar or a table cell.
-
Contrast on a ring.
.indicator-ringis a surface-coloured ring, so a dot on an unusual background gets a halo in the wrong colour — the same caveat as the avatar stack.
Right to left
The badge is positioned with logical insets, so it moves to the opposite corner under
dir="rtl" without a rule — which is correct: a notification count belongs
on the side the reader's eye finishes on.
<span dir="rtl" class="with-indicator">
<button class="btn btn-icon btn-ghost" aria-label="الإشعارات، ٣ غير مقروءة">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#bell"></use></svg>
</button>
<span class="indicator-badge" aria-hidden="true">٣</span>
</span>
<span dir="rtl" class="with-indicator">
<button class="btn btn-icon btn-ghost" aria-label="الإشعارات، ٣ غير مقروءة">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#bell"></use></svg>
</button>
<span class="indicator-badge" aria-hidden="true">٣</span>
</span>
Reduced motion
Nothing here animates. If you want a new notification to pulse, that is
.ping, which is reduced-motion guarded — and it
is worth remembering that a pulsing badge is motion in the corner of the reader's eye
for as long as the count is non-zero.
Printing
Neither is in src/99-print.css, and both are backgrounds — so a printed
dot disappears entirely and a printed badge prints its number with no fill. For a
status page that means the dots vanish and only the words remain, which is survivable
precisely because the words were required anyway.
Overriding it
@layer app.components {
/* A larger badge for two-digit counts */
.indicator-badge { min-inline-size: 1.4em; padding-inline: .35em; }
/* A dot tone Deck does not ship */
.indicator-info { background: var(--ink-400); }
}
When not to use it
-
Not for a status with a name. "Paid", "Overdue" — that is a
.badge, which shows the word. - Not as the only sign of something new. A badge appearing is silent. If it matters, announce it.
- Not for a count over about 99. The badge grows past its control. Cap it — "99+" — as every platform does.
- Not inline where a word would do. A dot in a sentence is a decoration the reader has to decode; the word beside it is doing the work anyway.
- Not on a control with no accessible name. The badge cannot supply one, and an icon button without a label is already the defect.