Icons are inlined as SVG at build time, not fetched. This page is the contract — how to place one, how it is sized and coloured, and what a new glyph has to satisfy. The 98 icons themselves are listed on the Brand Book's iconography page, which is generated from the icon directory; to copy, recolour or export one, use the Icon Library.
Placing an icon
Write the shortcode in any cms/ page and the generator inlines the file.
{{icon:close}}
It renders as the wrapper plus the glyph, with the accessibility attribute already set:
<div class="svg-icn">
<svg data-icon="close" aria-hidden="true" width="100%" height="100%" viewBox="0 0 24 24" fill="none">…</svg>
</div>
Three things are worth knowing about that output. The <svg> is set to 100 % of its wrapper, so the wrapper controls the size. data-icon carries the name, and it is a real CSS hook rather than a label — some component rules select on it. And aria-hidden="true" is applied automatically, which is right for a decorative glyph and means an icon-only control must carry its own label.
The wrapper
| Property | Value | Why |
|---|---|---|
width / height |
One token sizes the icon; the SVG fills whatever the wrapper is | |
aspect-ratio |
1 / 1 |
Every glyph is square, so the box stays square whatever the size token becomes |
display |
flex, centred |
Centres a glyph that does not fill its own viewBox |
margin |
-0.175em |
Optical correction, so an icon beside text sits on the same line rather than pushing it |
--icon-size is 1.3rem by default. Inside a it becomes 0.9em, so an icon scales with the button's text rather than staying a fixed size — which is why a small button's icon looks right without anything being set. The exception is [data-icon-only], which puts it back to a fixed 1.3rem so a square icon button keeps a legible glyph at every button size.
Colour
Every glyph is filled with currentColor, so an icon takes the colour of the text around it and needs nothing set on it.
- Never set
fillon an icon. Colour it by colouring its container; a hardcoded fill stops following the theme and will be wrong in dark mode.
What a glyph has to satisfy
All 98 icons in the set hold to four invariants, and the build checks the set for duplicates on every run.
| Invariant | Why it matters |
|---|---|
A 0 0 24 24 viewBox |
The wrapper assumes a square; a different viewBox renders at the wrong optical size beside its neighbours |
fill="currentColor" on every path |
This is what makes colour inherit |
No stroke anywhere |
A stroke-only path comes out solid when the colour pass runs over it, and scales its weight when the icon is resized |
| A unique drawing | Two names for one drawing is a warning at build time. Derive a variant by baking the change into the coordinates, never by copying a file or wrapping it in a transform |
fill="none" on the <svg> root |
The fill belongs on the paths; a root fill paints the whole box |
No xmlns attribute |
The file is inlined into a page, never served standalone, and the generator does not strip it — so a hand-added file has to arrive without one |
Stems are 2 units throughout, which measures 1.98 perpendicular on a 45° bar. A new glyph that does not match that weight will read as a different family at small sizes.
Accessibility
An icon is decorative until it is the only thing carrying the meaning.
<button class="button" data-icon-only aria-label="Close">{{icon:close}}</button>
- An icon-only control needs
aria-label. The glyph isaria-hidden, so without one the control announces as "button" and nothing else. - An icon beside its own text needs nothing. The text is the label; the glyph is decoration and should stay hidden.
Naming
Names are lowercase, hyphenated, and describe the drawing rather than the job it is doing — close, not dismiss-dialog, because the same glyph closes other things too. Size variants carry the size last: close, close-small, close-large.
An engine gap worth knowing
Six synced artefacts draw their own glyphs in CSS or in a JavaScript string rather than reading the icon directory: design-system.css carries a check, docs-kit/docs.css a check and a copy, both copy-button.js files build a check, docs-copy-chrome.js builds check, copy and download, and toast.js builds a close.
So every code-block copy button, every toast dismiss and every styled checkbox shows the engine's artwork rather than BlackDoctor's, and it cannot be fixed here — those files are overwritten on npm install. It is recorded under Engine gaps in docs/figma-mapping.md and the fix belongs upstream.
Rules
- Never inline a glyph's path data by hand. Use
{{icon:...}}so the page tracks the icon directory; a pasted copy drifts the moment the file changes, and the build cannot see it happen. - Never add an icon by editing the Brand Book page. That page is generated by
tools/icon-registry.pyfrom the directory — add the file, add its name to the script's categories, then re-run it. - Check a new name against the set first. A second name for an existing drawing warns at build time and splits the vocabulary in two.