⚠️ Design system CSS not found. Check the path in cms/docs.config.js → designSystemPath, then re-run npm run docgen.

Iconography

The wrapper, the shortcode and the rules the set holds to

Website / Iconography
Download .md file
Open .md in new tab
On this page
  • Placing an icon
  • The wrapper
  • Colour
  • What a glyph has to satisfy
  • Accessibility
  • Naming
  • An engine gap worth knowing
  • Rules

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.

Primary — the icon follows the text colour

Secondary

Accent

  • Never set fill on 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 is aria-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.py from 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.
On this page
  • Placing an icon
  • The wrapper
  • Colour
  • What a glyph has to satisfy
  • Accessibility
  • Naming
  • An engine gap worth knowing
  • Rules
Previous Motion
Next Code

© 2026 BlackDoctor