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.
Overview
-
1
Simple · M
-
2
Simple · L
-
3
Number · M
-
4
Number · L
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
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
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
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
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
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
-
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
-
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
-
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.
Accessibility
-
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 politeUIAccessibilityannouncement — 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.