Icon

.icon is sized in em, not pixels — so it scales with the text it sits beside instead of needing a size for every context — and it is stroked in currentColor, so it takes the colour of whatever it is inside. Those two decisions are why an icon in a button, a muted caption and a danger alert all look right without a variant for each.

When to use it

Put it on an <svg> containing a <use> that references the sprite. That is the whole pattern — there is no icon font, no per-icon class, and no build step that inlines anything.

One class, and it takes the colour and size of its surroundings

Sits on the baseline of the text beside it

<p class="cluster cluster-tight"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check-circle"></use></svg> Sits on the baseline of the text beside it </p> <button class="btn btn-primary"> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#download"></use></svg> Download </button>
<p class="cluster cluster-tight">
  <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check-circle"></use></svg>
  Sits on the baseline of the text beside it
</p>
<button class="btn btn-primary">
  <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#download"></use></svg>
  Download
</button>

vertical-align: -.22em is the number that makes it land on the baseline rather than floating above it. flex: 0 0 auto stops it shrinking when the text beside it is long.

Sizes

Icon sizes
ClassSizeUse
.icon-sm 1em Beside small text. Uses the heavier cut of the sprite.
.icon 1.25em The default. Lines up with body text.
.icon-lg 1.5em A button that is mostly icon.
.icon-xl 2em An empty state, a feature row.
Note the first one references #star-sm, not #star
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star-sm"></use></svg> <svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg> <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg> <svg class="icon icon-xl" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg>
<svg class="icon icon-sm" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star-sm"></use></svg>
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg>
<svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg>
<svg class="icon icon-xl" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg>

Why .icon-sm needs a different symbol

The sprite's symbols are filled outlines rather than strokes, so stroke-width does nothing to them — an icon shrunk to 1em would get visually lighter with no way to compensate.

So every icon ships twice: #name at weight 400 and #name-sm at weight 500, generated from the variable font by tools/icons/build-icons.mjs. .icon-sm is meant to be paired with the -sm symbol.

CSS cannot rewrite a <use href>, so the pairing is on you. .icon-sm with #star renders the light cut at a small size and simply looks thin — nothing errors, and nothing warns. This is the one part of Deck's icon system that a reader has to remember rather than being handed.

Fill and muted

.icon-fill swaps stroke for fill, for the solid cut of a glyph — a filled star for a rating, a filled circle for a status. .icon-muted drops the colour to --text-faint.

Outline, filled, muted
<svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg> <svg class="icon icon-lg icon-fill" style="color:var(--warn-500)" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star-fill"></use></svg> <svg class="icon icon-lg icon-muted" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg>
<svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg>
<svg class="icon icon-lg icon-fill" style="color:var(--warn-500)" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star-fill"></use></svg>
<svg class="icon icon-lg icon-muted" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#star"></use></svg>

Icon tiles

.icon-tile is a rounded square with a tinted background, for a feature row or an empty state — the icon equivalent of an avatar. Three status variants and a large size.

<span class="icon-tile"><svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#folder"></use></svg></span> <span class="icon-tile icon-tile-good"><svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg></span> <span class="icon-tile icon-tile-warn"><svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#alert-triangle"></use></svg></span> <span class="icon-tile icon-tile-bad"><svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#x"></use></svg></span> <span class="icon-tile icon-tile-lg"><svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#upload"></use></svg></span>
<span class="icon-tile"><svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#folder"></use></svg></span>
<span class="icon-tile icon-tile-good"><svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg></span>
<span class="icon-tile icon-tile-warn"><svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#alert-triangle"></use></svg></span>
<span class="icon-tile icon-tile-bad"><svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#x"></use></svg></span>
<span class="icon-tile icon-tile-lg"><svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#upload"></use></svg></span>

.icon-spin and .icon-follow

.icon-spin rotates continuously — a loading state on an icon that is not a .spinner. .icon-follow steps three pixels forward when its parent is hovered, which is what makes the chevron in a list row or a card link feel like it is going somewhere.

<div class="list" style="max-inline-size:24rem"> <a class="list-row" href="#motion-classes"> <div class="list-main"><span class="list-title">Hover this row</span></div> <svg class="icon icon-follow list-trail" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#chevron-right"></use></svg> </a> </div> <span class="cluster cluster-tight"> <svg class="icon icon-spin" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#refresh"></use></svg> Working </span>
<div class="list" style="max-inline-size:24rem">
  <a class="list-row" href="#motion-classes">
    <div class="list-main"><span class="list-title">Hover this row</span></div>
    <svg class="icon icon-follow list-trail" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#chevron-right"></use></svg>
  </a>
</div>
<span class="cluster cluster-tight">
  <svg class="icon icon-spin" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#refresh"></use></svg>
  Working
</span>

Classes

Eleven files style an icon, which is what happens to a primitive used everywhere. Only two of them are about the icon itself; the rest position it inside another component.

Classes, generated from the stylesheet source
Class Layer What it does Source
.icon deck.mobile An SVG icon sized in em, so it scales with the text it sits beside and lands on the baseline. Icons are filled with currentColor and inherit the surrounding colour. 08-mobile.css:134
.icon-fill deck.mobile Sets fill, stroke. 08-mobile.css:156
.icon-follow deck.motion Sets transition. 16-motion.css:254
.icon-lg deck.mobile A 1.6em icon, for icon tiles and empty states where the default would look undersized. 08-mobile.css:154
.icon-muted deck.mobile Sets color. 08-mobile.css:157
.icon-sm deck.mobile Small icons reference the heavier cut of the sprite, not a thicker stroke: the symbols are filled outlines, so stroke-width does nothing to them. Every icon ships twice, #name at wght 400 and #name-sm at wght 500, and .icon-sm goes with the -sm symbol. CSS cannot rewrite a <use href>, so the pairing lives in the markup: <svg class="icon icon-sm"><use href="deck-icons.svg#check-sm"></use></svg> 08-mobile.css:151
.icon-spin deck.mobile Sets animation. 08-mobile.css:158
.icon-tile deck.mobile Icon in a tinted tile — for feature rows and empty states 08-mobile.css:161
.icon-tile-bad deck.mobile Sets background, color. 08-mobile.css:173
.icon-tile-good deck.mobile Sets background, color. 08-mobile.css:171
.icon-tile-lg deck.mobile Sets inline-size, block-size, border-radius. 08-mobile.css:174
.icon-tile-warn deck.mobile Sets background, color. 08-mobile.css:172
.icon-xl deck.mobile Sets inline-size, block-size, stroke-width. 08-mobile.css:155
.is-open deck.components No declarations. 11-combobox.css:73

Tokens

An icon reads almost nothing. It inherits color and font-size from its parent, which is the entire design.

Design tokens this component reads
Token Live Declared value
--text-faint light-dark(var(--ink-500), var(--ink-500))
--brand-soft light-dark(var(--brand-50), oklch(26% .05 var(--hue-brand)))
--brand-soft-text light-dark(var(--brand-800), var(--brand-200))
--good-100 oklch(94% calc(var(--chroma-status) * .3) var(--hue-good))
--warn-100 oklch(95% calc(var(--chroma-status) * .32) var(--hue-warn))
--bad-100 oklch(94.5% calc(var(--chroma-status) * .3) var(--hue-bad))
--r-md 12px

Accessibility

  • Nearly every icon should be aria-hidden="true". An icon beside a label is decoration — the label is the name. Every example on this page hides its icons for that reason.
  • An icon-only control needs a label on the control. aria-label on the <button>, not on the SVG. The button is what gets focus and what is announced.
  • An icon that carries meaning on its own needs role="img" and a title — a status glyph in a table cell with no text beside it, for instance. That is rare, and usually a sign the cell should have text.
  • Colour is not a status. A green tick and a red cross are the same shape to a screen reader and similar to some readers with colour vision deficiency. The .icon-tile-* variants are a tint on top of a glyph, not a substitute for words.
  • currentColor means contrast is inherited. An icon is as readable as the text around it, automatically — which is one of the quiet benefits of stroking rather than filling with a fixed colour.
  • Icons scale with zoom because they are sized in em. A pixel-sized icon stays put while the text around it grows, which at 200% looks broken.
  • .icon-spin is exempt from the reduced-motion reset. src/02-reset.css keeps it turning at 2.4 seconds rather than freezing it, on the grounds that a frozen loading indicator reads as a hung page.

Right to left

This is the most interesting rule in the component. src/19-logical.css mirrors directional glyphs by matching on the sprite id itself:

[dir="rtl"] :is(
  .icon:has(use[href$="#chevron-left"]),
  .icon:has(use[href$="#arrow-right"]),
  .icon:has(use[href$="#send"]),
  …
) { scale: -1 1; }

So an arrow, a chevron and the send glyph flip automatically, while a checkmark, a clock and a magnifier — which would look wrong mirrored — are left alone. It is a list rather than a rule, which means a directional icon you add yourself is not in it; reach for .mirror-rtl in that case.

The chevron and the send glyph flip; the tick and the clock do not
<div dir="rtl" class="cluster"> <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#chevron-right"></use></svg> <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#send"></use></svg> <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg> <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#clock"></use></svg> </div>
<div dir="rtl" class="cluster">
  <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#chevron-right"></use></svg>
  <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#send"></use></svg>
  <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#check"></use></svg>
  <svg class="icon icon-lg" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#clock"></use></svg>
</div>

Reduced motion

.icon-follow is declared inside @media (prefers-reduced-motion: no-preference), so a reader who has asked for less motion never receives the rule at all. .icon-spin is the opposite case: it is in the reset's exemption list and keeps moving, slowly, because a frozen spinner reads as broken.

Printing

Icons print. They are strokes in currentColor, so they come out as line art at whatever the text colour prints as — which is usually correct.

.icon-tile is in the print-color-adjust: exact list in src/99-print.css, alongside badges, avatars and chart fills — so a tile keeps its tint on paper rather than printing as a glyph floating in white space. That is the difference between a status tile that still reads as a status and one that does not.

Overriding it

/* One icon, no layer needed */
<svg class="icon" style="inline-size:2.5em;block-size:2.5em">

@layer app.components {
  /* A lighter stroke throughout */
  .icon { stroke-width: 1.5; }

  /* A size Deck does not ship */
  .icon-2xl { inline-size: 3em; block-size: 3em; }
}

Changing stroke-width affects only the stroked symbols. The sprite's glyphs are filled outlines, so a heavier stroke does not make them heavier — that is what the two weights are for.

When not to use it

  • Not as the only label. An icon-only button is fine with aria-label, and a row of unlabelled glyphs is a guessing game for everyone.
  • Not for a logo or a brand mark. Those are hand-authored symbols in the sprite — #deck-mark and #deck-wordmark — and they are filled rather than stroked, so .icon's stroke settings do nothing useful for them.
  • Not for an illustration. A 24-unit grid at 1.25em is not a picture. Use an <img>.
  • Not for an emoji. Use .emoji, which sizes and aligns a character rather than an SVG.
  • Not with a fixed pixel size. The em sizing is the component. Overriding it with pixels breaks the alignment with text and the behaviour under zoom.