Badges
components specs/components/badges.kmd
Small visual marker attached to another element, signalling presence of notifications, unread count, or status. Material parity (`/components/badges`). Covers small (dot) and large (numeric) variants, anchoring rules, and overflow formatting.
Quando esta spec se aplica
Triggers primários
- Add a badge
Todos os triggers
- Add an unread count to an icon
- Show notification indicator on a button
- Mark presence / status with a colored dot
Corpo da especificação
Spec — Badges
Facet Visual of Koder Design. Material parity: https://m3.material.io/components/badges.
2 variants
| Variant | Size | Content | Use |
|---|---|---|---|
| Small (dot) | 6 px circle | None | Presence-only marker |
| Large (numeric) | 16 px height | 1-3 digits or text | Count or short label |
Pick variant by need: dot when count is irrelevant ("you have new messages"); numeric when count itself is information ("3 unread").
Anatomy (large badge on icon button)
┌───┐
┌─── │ 3 │ ← badge (top-right anchor)
│ └───┘
▼
┌─────┐
│ ✉ │ ← host (icon button)
└─────┘
- Dot diameter: 6 px
- Large height: 16 px
- Large min-width: 16 px (square at 1 digit); auto-expands
- Large padding: 4 px horizontal
- Text:
label-small(11/16, weight 600) - Background:
errorcolor role by default - Foreground:
on-error - Border: 1 px
surfacebackground (separation from host)
R1 — Anchor position
| Anchor | Offset | Use |
|---|---|---|
| Top-right (default) | -2 px x, -2 px y | Icon buttons, list items, avatar |
| Top-left | +2 px x, -2 px y | RTL mirror of top-right |
| Inline | After text, 4 px gap | Inside list row, after label |
Never anchor below or center — badge always sits at top corner OR inline after text.
R2 — Numeric formatting
| Count | Display |
|---|---|
| 0 | Hide badge entirely (don't show "0") |
| 1-9 | Single digit 3 |
| 10-99 | Two digits 42 |
| 100-999 | Three digits 512 |
| 1000+ | 999+ (cap) |
Custom cap allowed per surface: 99+ for notifications, 9+ for
inbox folders. Document per surface; pick one.
R3 — Color roles
| Meaning | Background | Foreground |
|---|---|---|
| Error / urgent | error | on-error |
| Warning | warning (extended) | on-warning |
| Info / neutral count | primary | on-primary |
| Success / new | success (extended) | on-success |
| Status — online | success dot | — |
| Status — away | warning dot | — |
| Status — offline | outline dot | — |
Default to error for unread/attention counts (matches Material
default and user expectation).
R4 — Animation
- Appear: fade-in + scale 0 → 1 (motion-fast, ~150 ms)
- Count change: cross-fade old → new digit (motion-fast)
- Disappear: fade-out (motion-fast)
- No bouncing / pulsing — distracting and ignored by users after habituation
- Reduced motion: instant appear/disappear, no animation
R5 — Accessibility
- Badge content is part of host's accessible name:
- Icon button:
aria-label="Inbox, 3 unread"(concat, not separate) - Or use
aria-describedbyreferencing a hidden span with badge text
- Icon button:
- Dot-only badge: include presence in host label ("Profile, online")
- Color alone never carries meaning — always pair with text or icon-shape difference
- Hit target: badge is decorative; host's hit target stays unchanged
R6 — Hit target
Badge is non-interactive. Tapping it triggers the host's action. Don't add a separate click handler on the badge — defeats user expectation and confuses screen readers.
R7 — Density
| Density | Dot | Large | Padding |
|---|---|---|---|
| Compact | 4 px | 14 px | 3 px |
| Default | 6 px | 16 px | 4 px |
| Comfortable | 8 px | 20 px | 6 px |
Inherits surface density from customization.kmd.
R8 — Per-preset variation
| Preset | Visual |
|---|---|
material3 | Rounded pill, no border, error-color default |
material2 | Circle (numeric), filled background |
ios_cupertino | Red filled circle, white text, slight gradient |
gnome | Rectangle with 4 px radius, accent color |
windows_11 | Pill shape, system accent color, subtle outline |
brutalist | Sharp square, thick 2 px border, no animation |
terminal_classic | ASCII suffix (3) next to icon, no shape |
R9 — Forbidden patterns
- ❌ Showing
0— hide badge entirely instead - ❌ Animating count change with bounce/pulse (distracting)
- ❌ Putting interactive elements (buttons, links) inside a badge
- ❌ Using badge to display arbitrary long text — use chip instead
- ❌ Color-only differentiation (status dot WITHOUT label text)
- ❌ Anchoring at bottom or center of host
- ❌ Stacking multiple badges on the same host
Cross-link
themes/color-roles.kmd—error/success/warningtoken sourcesthemes/typography.kmd—label-smallfor numeric textfoundations/elements.kmd— Marker familycomponents/chips.kmd— sibling for longer textual labels with interaction
Requirements (testable)
Requirement: Numeric badge formatting and overflow cap {#req-badges-numeric-formatting}
A numeric badge SHALL render 1-3 digits verbatim for counts 1 through 999, SHALL cap counts of 1000 or greater to the surface's overflow token (default 999+), and SHALL NOT render at all when the count is 0.
Scenario: Single-digit count renders verbatim
- GIVEN a large numeric badge bound to an unread count
- WHEN the count is set to 3
- THEN the badge is visible
- AND its text content is exactly
3
Scenario: Count over the cap collapses to overflow token
- GIVEN a large numeric badge using the default overflow cap
- WHEN the count is set to 1000
- THEN the badge text content is exactly
999+
Scenario: Zero count hides the badge
- GIVEN a large numeric badge currently showing
5 - WHEN the count changes to 0
- THEN the badge is not rendered (no
0is displayed)
Requirement: Anchor position {#req-badges-anchor}
A badge SHALL be anchored only at a top corner of its host (top-right by default, top-left for RTL mirror) or inline after text, and SHALL NOT be anchored at the bottom or center of the host.
Scenario: Default anchor is top-right
- GIVEN a large badge attached to an icon button host
- WHEN the badge is rendered with default anchoring
- THEN the badge is positioned at the top-right of the host with a -2 px x and -2 px y offset
Scenario: Bottom and center anchors are rejected
- GIVEN a badge attached to a host
- WHEN an anchor of
bottomorcenteris requested - THEN the badge does not render at that position (only top-corner or inline anchors are honored)
Requirement: Status meaning never conveyed by color alone {#req-badges-a11y}
A badge SHALL expose its content as part of the host's accessible name, and status information SHALL NOT be carried by color alone — a status dot SHALL always be paired with label text or an icon-shape difference.
Scenario: Numeric badge folds into host accessible name
- GIVEN an icon button host labelled "Inbox" with a numeric badge showing
3 - WHEN a screen reader computes the host's accessible name
- THEN the accessible name includes the badge count (e.g.
Inbox, 3 unread) as a single concatenated name
Scenario: Status dot requires accompanying text
- GIVEN a dot-only badge indicating an online presence status
- WHEN the badge is rendered
- THEN the presence is also present in the host's accessible label (e.g.
Profile, online) - AND the meaning is not conveyed by the dot color alone
Requirement: Badge is non-interactive {#req-badges-hit-target}
A badge SHALL be decorative and non-interactive: it SHALL NOT register its own click handler, SHALL NOT alter the host's hit target, and SHALL NOT contain interactive elements such as buttons or links.
Scenario: Tapping the badge triggers the host action
- GIVEN an icon button host with a numeric badge anchored at its top-right
- WHEN the pointer taps the badge region
- THEN the host's action is invoked
- AND no separate badge click handler fires
Requirement: Motion respects reduced-motion preference {#req-badges-animation}
A badge SHALL animate appearance, count change, and disappearance with the motion-fast fade/scale/cross-fade transitions and SHALL NOT bounce or pulse; when the user prefers reduced motion, the badge SHALL appear and disappear instantly with no animation.
Scenario: Reduced motion disables badge animation
- GIVEN a badge on a host with the reduced-motion preference enabled
- WHEN the badge appears in response to a new count
- THEN the badge becomes visible instantly with no fade or scale animation
Scenario: Count change never bounces
- GIVEN a large numeric badge showing
3with default motion - WHEN the count changes to
4 - THEN the digit updates via a cross-fade
- BUT no bounce or pulse animation is played
Requirement: Variant selection between dot and numeric {#req-badges-variants}
A badge SHALL be rendered as either the small dot variant (6 px circle, no content) for presence-only markers or the large numeric variant (16 px height, 1-3 digits or short text) for counts, and the large variant SHALL NOT be used to display arbitrary long text.
Scenario: Dot variant carries no content
- GIVEN a badge configured as the small dot variant
- WHEN the badge is rendered at default density
- THEN it is a 6 px circle
- AND it contains no text content
Scenario: Long text is refused by the numeric variant
- GIVEN a large badge variant
- WHEN it is asked to display an arbitrary long text string
- THEN the badge does not render the long text (a chip is used instead)
Referências
specs/foundations/elements.kmdspecs/themes/color-roles.kmdspecs/themes/typography.kmd