--- title: "Iconography" subtitle: "The wrapper, the shortcode and the rules the set holds to" description: "How icons work on the web: the shortcode that inlines them, the wrapper that sizes them, and the invariants that every glyph in the set has to hold to." author: "Studio" section: "Website" subsection: "Foundations" order: 9 status: "published" --- 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](/brand/iconography.html), which is generated from the icon directory; to copy, recolour or export one, use the [Icon Library](/tools/icon-library.html). ## Placing an icon Write the shortcode in any `cms/` page and the generator inlines the file. ```html {{icon:close}} ``` It renders as the wrapper plus the glyph, with the accessibility attribute already set: ```html
```
{{icon:close}} {{icon:search}} {{icon:arrow-right}} {{icon:download}} {{icon:copy}} {{icon:settings}} {{icon:info}} {{icon:menu}}
Three things are worth knowing about that output. The `` 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` | `var(--icon-size)` | 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 `.button` 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.

{{icon:info}} Primary — the icon follows the text colour

{{icon:info}} Secondary

{{icon:info}} 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 `` 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. ```html ``` - **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.