Icons are inline SVG elements wrapped in a container class. They inherit colour from the surrounding text, maintain a fixed square aspect ratio, and are accessible by default.
Principles
- Inline SVG only: never use
<img>tags for icons. Inline SVGs allow colour inheritance and eliminate extra HTTP requests. currentColoralways: all icon<path>elements must usefill="currentColor"so the icon inherits the parent's text colour. This keeps icons visually consistent across themes and contexts.- No
xmlns: stripxmlnsandxmlns:xlinkattributes from inline SVGs. They are only needed for standalone files, not inline usage. - Square aspect ratio: icons are always 1:1. Use
viewBoxto define the coordinate system, notwidth/height. - Brand icons only: do not use Material Design, Font Awesome, Heroicons, Feather, or any other third-party icon source. Use the brand icons from
assets/images/svg-icons/. If no icon exists for your need, request one from the design team.
Icon Wrapper
Use to wrap inline SVG icons. This class constrains the icon to a fixed size and enforces the square aspect ratio.
<div class="svg-icn" data-icon="arrow-right">
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none">
<path d="M5 12h14M12 5l7 7-7 7" fill="currentColor"/>
</svg>
</div>
Properties
| Property | Value | Purpose |
|---|---|---|
width |
1.5rem |
Standard icon size |
height |
1.5rem |
Matches width for square |
display |
flex |
Enables centering |
justify-content |
center |
Horizontal centering |
align-items |
center |
Vertical centering |
aspect-ratio |
1 / 1 |
Enforces square shape |
data-icon attribute
Always include a data-icon attribute with a descriptive name. This makes icons identifiable in code. Raw SVG paths are unreadable without it.
<div class="svg-icn" data-icon="chevron-down">...</div>
<div class="svg-icn" data-icon="close">...</div>
<div class="svg-icn" data-icon="search">...</div>
Use lowercase kebab-case for icon names (e.g. arrow-right, chevron-down, arrow-top-right).
Logos
Logos follow the same data-attribute pattern as icons, using data-logo instead of data-icon. The wrapper uses a generic class, and the logo name goes on the <svg> element.
<div class="svg-logo" style="aspect-ratio: 1050 / 505">
<svg data-logo="by-default-centered" viewBox="0 0 1050 505" width="100%" height="100%" fill="none" aria-hidden="true">
<path d="..." fill="currentColor"/>
</svg>
</div>
Key differences from icons
| Icons | Logos | |
|---|---|---|
| Wrapper class | ||
| Data attribute | data-icon="name" |
data-logo="name" |
| Aspect ratio | Fixed 1:1 via CSS | Preserved via inline style="aspect-ratio: W / H" |
| Size | Fixed (1.3rem default) | Responsive (width set by context) |
CSS targeting
Use :has() to target the wrapper based on a specific logo:
.svg-logo:has([data-logo="by-default-centered"]) {
width: var(--sidebar-logo-width);
}
SVG Cleaner workflow
npx svg-cleaner --logo --logo-name brand-name --current-color --strip-comments
This outputs a <div class="svg-logo"> wrapper with data-logo="brand-name" on the <svg> element.
Icon in Buttons
For icon-only buttons, add the data-icon-only attribute and as the SVG wrapper. Always include aria-label.
<button class="button" data-icon-only aria-label="Close">
<div class="svg-icn">
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" fill="currentColor"/>
</svg>
</div>
</button>
See the Button documentation for full button modifier details.
SVG Requirements
Every icon SVG must follow these rules before being added to the codebase:
| Rule | Required | Example |
|---|---|---|
fill="currentColor" |
Yes | Inherits text colour from parent |
viewBox attribute |
Yes | viewBox="0 0 24 24" defines the coordinate space |
No xmlns |
Yes | Strip xmlns and xmlns:xlink, not needed inline |
width="100%" height="100%" |
Yes | SVG fills its wrapper; sizing is controlled by the wrapper class |
fill="none" on <svg> |
Yes | Prevents default black fill; paths use fill="currentColor" individually |
| No XML comments | Recommended | Remove <!-- ... --> comments for cleaner code |
Standard viewBox
Use 0 0 24 24 as the default icon grid. If an icon uses a different coordinate system, preserve its original viewBox. Do not rescale manually.
Icon Sprite
Products consuming the system as a package receive the whole registry as one file: icons.svg, a <symbol> sprite built from every icon in assets/images/svg-icons/. It ships alongside design-system.css in the design-system-dist artefact repo.
Symbol ids are the lowercase kebab-case icon names from the registry below — arrow-right, sidebar-left-open, and so on. Reference a symbol with <use> inside the standard wrapper:
<div class="svg-icn" data-icon="arrow-right">
<svg fill="none" width="100%" height="100%" aria-hidden="true">
<use href="/assets/icons/icons.svg#arrow-right"></use>
</svg>
</div>
Two rules carry over unchanged: the wrapper still controls sizing, and icons still inherit colour through currentColor. One new constraint: <use href> does not load cross-origin, so serve the sprite from your own domain — copy it into your project rather than hotlinking it.
Inside this repo, icons stay inline (the docs pages inline each SVG directly); the sprite is the delivery format for package consumers.
Icon Manifest & Consumer Sync
Package consumers control which icons they ship via icons.manifest.json at the project root. The bd-sprite CLI — run automatically by bd-sync on install — reads it and builds a project-sized sprite instead of shipping the full master set.
{
"output": "assets/icons/icons.svg",
"icons": ["arrow-right", "chevron-down", "close", "check", "search", "menu"],
"local": ["assets/icons/local"]
}
output— where the built sprite lands, relative to the manifesticons— design system icon names to include, matched against the registry below (drop the .svg extension)local— optional list of directories holding the project's own SVGs, merged into the same sprite
When to Subset
Most sites reference 15–45 of the 175 icons in the registry. The sprite carries
177 symbols — the extra two are deprecated names kept resolving for consumers
(icons.aliases.json) and deliberately hidden from the registry. The master sprite — every icon, unfiltered — is roughly 115KB; a subset sprite trims that to just what the site uses.
| Situation | What to do |
|---|---|
| Site uses a small, known set of icons | Add icons.manifest.json and list them — smallest possible sprite |
| Site is early, icon usage still shifting | Skip the manifest — bd-sync copies the full master sprite by default, no build step required |
| Site already has a manifest and adds an icon | Add the name to icons, re-run bd-sync |
Subsetting is opt-in: no manifest, no build. A project that never creates icons.manifest.json gets the full master sprite exactly as it always has.
Site-Local Icons
The local key points at directories of icons the project owns but the design system doesn't — one-off marks with no reason to live in the shared registry. Two rules are enforced at build time, not left to convention:
- Kebab-case only. A symbol id becomes both an XML attribute and a
<use href="#name">fragment — a name likeFrame 1 & 2produces a sprite no browser can parse, with no error surfaced. Local names must match the samea-z0-9-pattern as design system icons. - No collisions with design system names. A local icon named
closewould shadow the sharedclosesymbol depending on load order — invisible until the wrong icon renders somewhere. The build hard-fails on any collision rather than silently picking a winner.
Both checks are fail-fast by design: a build that quietly resolved the conflict would ship the wrong icon with nothing to chase.
Third-party brand marks are the canonical site-local case. The Google and Apple sign-in marks live at assets/icons/local/ in this repo — deliberately outside the shared registry, because a brand mark is a per-product licensing decision, not a system asset. They follow the same authoring rules as registry icons (24×24, monochrome currentColor paths, cleaned with npx svg-cleaner) and are inlined where used; the {{icon:name}} shorthand reads the shared registry only and will not resolve them. See Login → Social sign-in for usage.
Renamed Icons
An icon name is public API: it appears in consumer icons.manifest.json files this repo can't see. So a rename keeps the old name working rather than breaking those builds silently. The retired name stays on disk as a byte-identical source file and is recorded in icons.aliases.json at the project root:
{
"aliases": {
"switch": { "renamedTo": "loop", "since": "2.2.0" }
}
}
Listed names still build into the sprite and still resolve through a manifest, but they are hidden from the registry table, the Brand Book grid, and llms.txt — one glyph, one name, everywhere a person or a model goes looking. Alias files and their entries are deleted at the next major version.
Duplicate Detection
The docs generator compares the path data of every icon at build time, ignoring coordinate precision, and warns when two names resolve to the same drawing:
⚠️ Duplicate icon geometry: "docs" is the same drawing as "page" — one of them is redundant
Matching filenames were always caught; matching artwork was not, which is how a re-export saved under a new name could quietly become the registry's second copy of an existing icon. The check warns rather than fails — a deliberate near-duplicate is a judgement call, but it should be a conscious one.
Cursor Sprite (Separate Artifact)
dist/cursors.svg ships alongside the icon sprite but is never merged into it. Icon symbols are single-colour (fill="currentColor", inherited from the parent); the cursor glyphs used by bd-cursor's GRAPHIC mode are two-colour — an outline plus a fill — which currentColor can't express. bd-sync copies cursors.svg in full whenever it exists in the release; it isn't affected by icons.manifest.json subsetting, since cursor glyphs were never part of the icon registry to begin with.
Icon Font
SVG sprites are the delivery format for icons in this system. No icon font exists, and none is planned. Every icon source already lives as an individual SVG in assets/images/svg-icons/, so a font is technically possible — it would be generated from that same source folder rather than maintained as a separate asset. That only happens if a real context demands it, such as an email template that can't reference an external sprite via <use>. Until a context like that shows up, sprites stay the only delivery format.
Accessibility
- Decorative icons: add
aria-hidden="true"to the SVG when the icon is purely visual and accompanied by text. - Meaningful icons: add
aria-labelto the parent element (e.g. the button) when the icon conveys meaning without visible text. - Icon buttons: always include
aria-labelon the<button>element.
<!-- Decorative: icon next to text -->
<div class="svg-icn" data-icon="check">
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M20 6L9 17l-5-5" fill="currentColor"/>
</svg>
</div>
<!-- Meaningful: icon button with no visible text -->
<button class="button is-icon" aria-label="Close menu">
<div class="svg-icn">
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M18 6L6 18M6 6l12 12" fill="currentColor"/>
</svg>
</div>
</button>
Colour
Icons inherit colour through currentColor. To change an icon's colour, change the text colour of its parent. Never hardcode a fill value.
<!-- Icon inherits the faded text colour -->
<div class="text-faded">
<div class="svg-icn" data-icon="info">
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M12 2a10 10 0 100 20 10 10 0 000-20zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" fill="currentColor"/>
</svg>
</div>
</div>
Icon Shorthand
Use {{icon:name}} in any markdown file processed by the doc generator to render an inline icon. The shorthand is expanded at build time. The actual SVG is read from assets/images/svg-icons/ and injected as a standard wrapper.
{{icon:check}} → renders the check icon
{{icon:arrow-right}} → renders the arrow-right icon
{{icon:t-shirt}} → renders the t-shirt icon
The name must match a key from the icon registry below. If the name is not found, a warning is logged during generation and an HTML comment is output instead.
This shorthand only works in files processed by cms/generator/generate-docs.js. It does not work in standalone HTML pages.
A second placeholder renders the whole set at once: put {{icon-registry}} on a line of its own and the generator expands it into the full registry table. The Registry section below is built this way.
SVG Cleaner Tool
Use the SVG Cleaner to prepare icons before adding them to the codebase. The tool automates the required cleanup:
- Strips
xmlnsattributes - Sets fills to
currentColor(when enabled) - Wraps in with
data-iconattribute (when "Icon" is checked) - Strips XML comments
- Optional minification
For CLI usage:
echo '<svg>...</svg>' | npx svg-cleaner --current-color --icon --icon-name arrow-right --strip-comments
Naming Conventions
| Convention | Example |
|---|---|
| Lowercase kebab-case | arrow-right, chevron-down |
| Describe the shape, not the function | arrow-right not go-forward |
| Use directional suffixes | chevron-up, chevron-down, chevron-left |
| Use common icon vocabulary | close, search, menu, check, plus, minus |
Rules
| Do | Don't |
|---|---|
Use fill="currentColor" on all paths |
Hardcode hex colours in SVG fills |
| Use to wrap all icons | Use <img> tags for icons |
Include data-icon with a descriptive name |
Leave icons unnamed |
Include aria-hidden="true" on decorative icons |
Omit accessibility attributes |
Include aria-label on icon-only buttons |
Rely on the icon alone to convey meaning |
| Use the SVG Cleaner to prepare icons | Manually edit SVG attributes |
Strip xmlns from inline SVGs |
Keep attributes meant for standalone files |
Use width="100%" height="100%" on SVGs |
Use fixed pixel/rem sizes on the SVG element |
Use fill="none" on the <svg> element |
Omit fill on <svg> (defaults to black) |
| Use wrapper to control icon size | Size icons via width/height on the SVG |
| Check the registry before using any icon | Use external icon libraries as a fallback |
Registry
The full brand icon set. This table is generated at build time from assets/images/svg-icons/, so it always matches the source directory. The data-icon value is the name to use with the wrapper, the shorthand, and the sprite. For a categorised view with copy and download, see the Brand Book iconography page.
| Icon | data-icon |
|---|---|
3d |
|
accessibility |
|
ad-left |
|
ad-right |
|
ad-top |
|
add |
|
ai |
|
ai-chat |
|
ai-large |
|
ar |
|
arrow-down |
|
arrow-left |
|
arrow-right |
|
arrow-top |
|
arrow-top-right |
|
arrow-up |
|
back-arrow |
|
bolt |
|
book |
|
book-open |
|
bookmark |
|
browser |
|
calendar |
|
catalog |
|
chart |
|
chat |
|
check |
|
check-circled |
|
check-squared |
|
checklist |
|
chevron-down |
|
chevron-down-large |
|
chevron-left |
|
chevron-left-large |
|
chevron-right |
|
chevron-right-large |
|
chevron-up |
|
chevron-up-large |
|
circled |
|
click-2 |
|
clock |
|
close |
|
close-circled |
|
close-large |
|
code |
|
cog |
|
copy |
|
credit-card |
|
cursor |
|
cursor-pointer-1 |
|
cursor-pointer-2 |
|
cursor-pointer-4 |
|
design |
|
design-system |
|
desktop |
|
docs |
|
download |
|
drag |
|
exclamation |
|
eye |
|
eye-off |
|
fast-forward |
|
file |
|
file-audio |
|
file-css |
|
file-doc |
|
file-font |
|
file-html |
|
file-img |
|
file-json |
|
file-md |
|
file-txt |
|
file-video |
|
filter |
|
flame |
|
flame-spark |
|
folder |
|
folder-open |
|
font-size |
|
frame |
|
full-screen |
|
game |
|
gamepad |
|
globe |
|
headphones |
|
heart |
|
home |
|
info |
|
instagram |
|
link |
|
linkedin |
|
list |
|
lock |
|
login |
|
logout |
|
loop |
|
loop-reverse |
|
mail |
|
map |
|
menu |
|
minus |
|
mobile |
|
moon |
|
more-horizontal |
|
more-vertical |
|
news |
|
open-full |
|
paintbrush |
|
partner |
|
pause |
|
pencil |
|
pencil-draw |
|
percentage |
|
photo |
|
photos |
|
pin |
|
play |
|
play-2 |
|
play-outline |
|
present |
|
question |
|
reddit |
|
refresh |
|
return-arrow |
|
rewind |
|
scroll-down |
|
search |
|
search-large |
|
send |
|
settings |
|
share |
|
share-1 |
|
shop |
|
shopping-bag |
|
sidebar-close-2 |
|
sidebar-left-close |
|
sidebar-left-open |
|
sidebar-right-close |
|
sidebar-right-open |
|
smiling |
|
sound |
|
sound-off |
|
sound-on |
|
spotify |
|
squared |
|
stack |
|
status-0 |
|
status-10 |
|
status-100 |
|
status-20 |
|
status-50 |
|
status-75 |
|
status-check |
|
sticky-bottom |
|
sticky-top |
|
sun |
|
sun-2 |
|
swap |
|
t-shirt |
|
tablet |
|
tap-3 |
|
target |
|
thumbs-down |
|
thumbs-up |
|
tiktok |
|
trash |
|
trending |
|
unlock |
|
user |
|
users |
|
video |
|
warning |
|
weapon |
|
widgets |
|
youtube |
Requesting a New Icon
If you need an icon that is not in the brand icon set:
- Check the brand book: the icon you need may exist under a different name. See the Visual Identity page for the full icon grid.
- Document the request: describe the concept, intended size, and where it will be used
- Submit to the design team: new icons must match the existing style (24x24 grid, single-colour, rounded corners)
- Do not use a placeholder: wait for the brand icon to be designed rather than shipping with a generic substitute
Maintaining a consistent icon language across the site is more important than shipping fast with mismatched icons.