v3.1.2
Indicator

A small non-interactive marker that shows a status or a count on top of another element. Two types — the Simple dot and the Number pill — on two sizes, always decorative: the parent carries the meaning.

3.1 3.1 -- 3.1.2

Overview

  • 1
    Simple · M
  • 2
    Simple · L
  • 3
    Number · M
  • 4
    Number · L
1
2
3
4

States

Indicator is non-interactive — Standard is the only rest form; there is no pressed, hover, or focus state. The lifecycle adds Hidden (count 0 renders nothing) and the two transit phases documented under Animation. The colour trio below is one contract for both sizes.

Standard

1
2
  • 1
    Fill --accent-red2 — both sizes, both types.
  • 2
    Count --text-and-icon-always-light — the count colour, via currentColor.Caption/Caption — the M text style.Body/Compact Body — the L text style.

Border

1
  • 1
    Off by default The Figma set ships Border=No; indicator--border turns the ring on. A 2dp ring drawn OUTSIDE the fill as a spread shadow — it never moves the layout and never shrinks the marker.
  • Ring token --background-primary — matches the page ground, so the ring reads as a cut-out from busy content underneath (the grey plate here exists to make it visible).

Custom colours

1
  • 1
    Re-point the trio --indicator-fill — the marker fill (here --accent-green2 and --accent-blue2).--indicator-content — the count colour.--indicator-border — the ring, consumed only with indicator--border.
  • No Custom set in Figma A non-red indicator is an instance-level fill override there; the custom properties are the web equivalent. The 4.5:1 count-contrast duty moves to whoever re-points the fill.

Anatomy

The Number pill is the full anatomy: a minimum-square box with one text child. Simple is the box alone — a bare dot with no children. The hidden border layer completes the family.

1
2
  • 1
    Box --sp-s24 — the L minimum square; the width re-hugs the count.--sp-s32 — the radius: always over half the height, so the box stays a full pill.
  • 2
    Count label The single text child — Body/Compact Body on L, Caption/Caption on M, capped at 99+. Colour via currentColor from the root.
  • Border layer A 2dp OUTSIDE ring, hidden by default — indicator--border shows it. Drawn as a spread shadow: no layout shift.
  • Simple form The box alone at --sp-s8 (M) / --sp-s12 (L) — no children at all.

Layout

Two fixed dots and two minimum squares that grow with the count. Every SP-bound value scales with the SP mode capped at ×130% — the Figma bindings sit on the 130%-ratio collection. The indicator ships no margins: the parent owns every offset.

Geometry

1
2
3
4
  • 1
    Simple M --sp-s8 — the dot, fixed.
  • 2
    Simple L --sp-s12 — the dot, fixed.
  • 3
    Number M --sp-s16 — the minimum square.--sp-s4 — the side padding once digits push past it.
  • 4
    Number L --sp-s24 — the minimum square.--sp-s6 — the side padding; "99+" shows the growth.
  • Radius --sp-s32 — always over half the height: a circle at one digit, a pill at more.

Positioning

LTR — the top-end corner
RTL — the same logical corner, mirrored
  • The parent owns the seat Margins are --sp-s0 — the indicator never positions itself. Free-form compositions use an absolutely-positioned anchor inside the parent, on logical insets so RTL mirrors by itself.
  • The DS seat is DecorateContainer For product compositions the DecorateContainer primitive carries the 3×3 anchoring, the offsets, and the RTL mirroring — Indicator is one of its predefined decorations.

Animation

Motion follows the motion system: the marker lives on the scaleS pattern, consumed from motion.css — the component declares nothing of its own.

Appear / Hide

Live — scaleS in, hold, scaleS out, on a loop
  • General var(--component-scale-s-appear) — appear: scale 0 → 1 + opacity, 200 ms.var(--component-scale-s-hide) — hide: the same pair back, 200 ms.Lifecycle classes: indicator--entering · indicator--visible · indicator--exiting · indicator--hidden.
  • Reduced motion Handled at the token: motion.css zeroes every --component-* inside prefers-reduced-motion: reduce — the marker appears and leaves instantly, with no component override.

Count change

Live — 1 → 9 → 42 → 99+ — the count rolls, the width glides with it
  • The numericText roll var(--transforming-state-fast) — each phase of the roll: the outgoing value up and out, the incoming one in from below.indicator__count — the optional count wrapper an animated composition uses; a static count stays bare text.The web mirror of iOS: contentTransition(.numericText()) on iOS 16+, the X-axis flip on iOS 15.
  • The width glides with it When digits come or go, the composition pins the measured widths and tweens between them on the same token (indicator--count-resizing) while the new value rolls in — the way SwiftUI animates the frame alongside numericText. A recorded deviation from the compositor-only guidance: a badge-sized layout tween, and the one place this component animates layout.

Usage

Use it for counts and status marks on another element: unread messages on a tab, a cart count on an icon, an attention dot on an avatar, the decoration slot of a DecorateContainer composition.

  • 1
    A marker, never an element of its own The indicator has no meaning without the element it marks — it is never placed standalone, and the tap belongs to the parent.
  • 2
    Numbers stay short 1–99 render as they are; anything above is 99+; zero renders nothing at all. A text label is a Tag, not an Indicator.
✓ A count on the control it belongs to — the parent carries the meaning
✕ The raw number — anything over 99 renders as 99+

Accessibility

A control: <button aria-label="Messages, 3 unread">
Static parent: role="img" — never on a tab or button
1
2
  • 1
    Always Decorative aria-hidden="true" on every indicator, both types — the digit in a Number pill is never read directly. Android: importantForAccessibility="no"; iOS: isAccessibilityElement = false.
  • 2
    The Parent Speaks The count or status lives in the parent's accessible name — aria-label="Messages, 3 unread" / contentDescription / accessibilityLabel. A dot's meaning is carried in words the same way.
  • Changes Announce Politely A dynamic count announces through a visually-hidden aria-live="polite" sibling region on the web; TalkBack uses the parent's polite live region, VoiceOver a polite UIAccessibility announcement — never by focusing the indicator.
  • Zero Means Gone Count 0 removes the element from the DOM — and with it from the accessibility tree; the parent's label drops the count phrase ("Messages", not "Messages, 0 unread").
  • Never Focusable No tabindex, no role, no touch target — the parent is the control. The scaleS motion carries no meaning: the count is always available as text through the parent.
  • Contrast — Known Shortfall White count text on --accent-red2 measures 3.98:1 Light / 3.47:1 Dark — below the 4.5:1 target (the palette pairing shared with Notification / HeaderAlert; a token-level call for the colour owner). The contract mitigates it: the count is always duplicated in the parent's accessible name.