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.

The word beside the dot is what carries the meaning

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.

The count is in the button's label, and the badge is hidden
AC
<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.

Classes, generated from the stylesheet source
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

Design tokens this component reads
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, and aria-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-live region — 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-ring is 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.