Icon
CSS-rendered icons: an inline-SVG mask-image painted by
currentColor — real, themable glyphs with zero
JS, zero font files, no network requests. Sized 1em, so they track
the surrounding font size (and density) automatically. The set is deliberately
small — enough ERP glyphs to prove the mechanism, never a catalogue; adding
your own is one line of CSS — or paste inline SVG with
fill="currentColor", which is an equally first-class answer.
Not as the only label for a meaning the reader has to
learn — an icon-only control works when the glyph is already a
convention (close, search, settings) and space is genuinely tight, and
then it still needs an aria-label, since these glyphs are
aria-hidden decoration. It fails the moment the meaning is
domain-specific: nobody recognises a mark for "post goods issue", so
that button needs its words. And where no glyph in any set reads as the
thing — "accounts payable" has none — use letters: an initials
badge is honest where an
approximate icon is confidently wrong.
The set
--doc --invoice --cart --check-circle --truck --box --chart --grid Convenience presets for ERP concepts common enough
to be worth shipping a name for — each is just a value of
--bo-icon-src, same as your own would be (next section).
Deprecated 4
--settings --barcode --building --user Kept shipping (removing published classes is a
next-major change, not a tidy-up) but no longer recommended: each was only
ever one example value of --bo-icon-src, so point that at your
own SVG instead (next section). Screens that already render one keep
working until the next major — deprecating a glyph deprecates the
recommendation, not the class.
Themable — it's just currentColor
Set color (or inherit it) and the glyph
follows — semantic tokens, dark mode, and brand presets all work with no
icon-specific wiring. Under forced-colors the icons repaint in the system text
color instead of vanishing (mask icons need an explicit opt-out; shipped).
Sizes — font-size IS the size
No size modifiers: 1em means an icon in
a compact table row is smaller than one in a spacious header for free.
In context — sidebar links and icon buttons
Composes with the existing
.bo-sidebar-nav__icon slot and .bo-btn--icon —
decorative icons carry aria-hidden; an icon-only button labels
itself with aria-label, never the icon.
Both buttons here pass the opener's test deliberately: a cross for
close and a bar chart for reports are conventions a reader already
holds. The same treatment on a domain action — "post goods issue" —
would not be, and that button gets its words. Neither glyph here is one
of the deprecated ones above — copy what this page still recommends.
Any icon you like — one custom property
The glyphs above are not a privileged list: each one is just a value of
--bo-icon-src. Point that property at any SVG and the element gets the
framework's sizing, currentColor painting, density tracking and
forced-colors handling with no new class from us.
View code
<span class="bo-icon" aria-hidden="true"
style="--bo-icon-src: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23000" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>')"></span>
<span>Add line</span> That works with Lucide,
Heroicons or your own brand mark — copy the SVG,
percent-encode # as %23, and set the property. Icons drawn
with stroke="currentColor" need no edit at all, because the mask ignores
the SVG's own colours and paints with yours.
Why there is no 200-icon catalogue here, since that is the obvious thing to ask for: the 26 glyphs shipped today are 10.5% of the whole framework by size, because every glyph is an inlined SVG. At that rate two hundred would add roughly 79 kB, against a whole framework that is 98 kB — to compete with icon sets that are better maintained and free. One property is the smaller answer and the more general one.
Your own artwork — which mechanism, and when none
Four questions come up as "can we have SVG for this?". Three already have an answer that ships, and the fourth is a no.
| You want | Use | Why |
|---|---|---|
| A one-colour glyph | --bo-icon-src on .bo-icon | It is a mask, so it takes your text colour and follows the theme, density and forced-colours handling automatically |
| A multi-colour mark — a logo, a vendor identicon | a plain <img>, inside the avatar if it is a person or an org | A mask cannot do this. It discards the artwork's own colours by design — that is what makes it themable. Multi-colour means an image |
| A per-tenant app tile | the app launcher's inline svg slot | Already the extension point: an inline <svg> inherits
currentColor exactly like the shipped glyphs |
| An initials avatar | no SVG at all — .bo-avatar with two letters in it | Text scales, translates, is selectable and needs no asset pipeline. Generating an SVG for it would be strictly worse |
What this framework will not ship: an illustration set. Empty
states and error screens are the places an ERP feels most broken, and the fix is
almost always a clear sentence and the action that resolves it — not a drawing.
A picture that has to be redrawn for every tenant's brand, translated for nothing,
and downloaded on a warehouse tablet is cost without an answer. If you want one,
it is an <img>, and it is alt="".
Markup
<!-- Decorative (label carries the meaning). bo-sidebar-nav__icon is the rail's
slot — it sizes the box and the collapsed rail is derived from it: -->
<a class="bo-sidebar-nav__link" href="/invoices">
<span class="bo-icon bo-icon--invoice bo-sidebar-nav__icon" aria-hidden="true"></span>
<span class="bo-sidebar-nav__label">Invoices</span>
</a>
<!-- Icon-only control labels ITSELF, never the icon: -->
<button class="bo-btn bo-btn--ghost bo-btn--icon" aria-label="Close">
<span class="bo-icon bo-icon--close" aria-hidden="true"></span>
</button>
<!-- Your own glyph in one line: point --bo-icon-src at any single-color SVG. -->
<style>
.my-icon--rocket { --bo-icon-src: url("data:image/svg+xml,<svg …></svg>"); }
</style>
<span class="bo-icon my-icon--rocket" aria-hidden="true"></span> Quick reference generated
| Class | Role |
|---|---|
| .bo-icon | block (root) |
| .bo-icon--align-center | variant: align-center |
| .bo-icon--align-left | variant: align-left |
| .bo-icon--align-right | variant: align-right |
| .bo-icon--barcode | variant: barcode |
| .bo-icon--box | variant: box |
| .bo-icon--building | variant: building |
| .bo-icon--cart | variant: cart |
| .bo-icon--chart | variant: chart |
| .bo-icon--check-circle | variant: check-circle |
| .bo-icon--clear-format | variant: clear-format |
| .bo-icon--close | variant: close |
| .bo-icon--doc | variant: doc |
| .bo-icon--format | variant: format |
| .bo-icon--grid | variant: grid |
| .bo-icon--indent | variant: indent |
| .bo-icon--invoice | variant: invoice |
| .bo-icon--keyboard | variant: keyboard |
| .bo-icon--link | variant: link |
| .bo-icon--list-bullet | variant: list-bullet |
| .bo-icon--list-numbered | variant: list-numbered |
| .bo-icon--outdent | variant: outdent |
| .bo-icon--quote | variant: quote |
| .bo-icon--save | variant: save |
| .bo-icon--settings | variant: settings |
| .bo-icon--truck | variant: truck |
| .bo-icon--user | variant: user |
API generated from CSS
| Blocks | bo-icon |
|---|---|
| Variants | bo-icon--align-centerbo-icon--align-leftbo-icon--align-rightbo-icon--barcodebo-icon--boxbo-icon--buildingbo-icon--cartbo-icon--chartbo-icon--check-circlebo-icon--clear-formatbo-icon--closebo-icon--docbo-icon--formatbo-icon--gridbo-icon--indentbo-icon--invoicebo-icon--keyboardbo-icon--linkbo-icon--list-bulletbo-icon--list-numberedbo-icon--outdentbo-icon--quotebo-icon--savebo-icon--settingsbo-icon--truckbo-icon--user |
| Component tokens | --bo-icon-src |
| JS required | None — CSS-only. |
- Painted by
currentColorviamask-image— themes, dark mode, and brand presets apply with zero icon-specific wiring; no new contrast pairs needed (an icon inherits the already-gated color of its text context). - Decorative icons (next to a visible label) are
aria-hidden="true"; icon-only controls label THEMSELVES (aria-labelon the button/link). - Deliberately a small set, not a library — add your own with one
--bo-icon-srcline, or paste inline SVG withfill="currentColor"; both are first-class. Set the custom property, notmask-imagedirectly:.bo-iconis in@layer bo-components, so amask-imageoverride only paints where the consumer rule outranks that layer, while the property has no competing declaration to lose to. - Forced-colors: mask icons vanish by default (the paint is a background-color, which forced colors override) — the shipped opt-out repaints them in
CanvasText.
Maturity generated
- Introduced
- v0.1.0
- CSS floor
- Chrome/Edge 99 · Firefox 97 · Safari 15.4
- Alignment scored
- 2026-08-23
- Assistive-tech evidence
- None recorded
Pin this component's first release with npm i @busy-office/ui@0.1.0.
The floor is derived from this component's stylesheet alone — set by
@layer — so it sits at or below the
whole framework's Chrome/Edge 119 · Firefox 129 · Safari 17.5.
No assistive-technology runtime result is recorded for any component: it needs a human listening to VoiceOver and NVDA on owner hardware; no gate in this repo can simulate announcement behaviour.
What automation covers instead is test:axe across every page at 1440 and 390, and check:forced-colors — see
accessibility.
Design-system alignment generated
100% (15/15) — how closely this component follows the design language. Each rule is scored 0-3 against a checkable citation; rules that don't apply leave the denominator. Scored 2026-08-23.
| Rule | Score | Evidence |
|---|---|---|
| Typography | 3 / 3 | 1em square, so glyphs track the surrounding font size and therefore density with no per-tier rule |
| Colour | 3 / 3 | painted by background-color: currentcolor; the %23000 inside the data URIs is SVG stroke (and fill, on --settings) under a MASK and is never painted |
| Spacing | 3 / 3 | the -0.125em baseline nudge explains itself — a 1em box rides high on the baseline, so it drops to centre on the x-height (94.6b) |
| Interaction | n/a | a decorative span; it introduces no interaction surface |
| Content | 3 / 3 | opener carries the wrong-choice clause: "Not as the only label for a meaning the reader has to learn" — icon-only works for conventions (close/search/settings) with an aria-label, fails for domain actions nobody has a mark for; initials badge where no glyph reads as the thing. Blind re-scored 2026-08-23 (Polish round 6): the scorer caught the first draft's absolute "Never" contradicting this page's OWN icon-only button demo, so the clause was narrowed to conventional-vs-learned meaning and the demo caption now states why its two buttons pass that test. |
| Fit | 3 / 3 | every shipped glyph is a value of one custom property (--bo-icon-src), so the set is an example of the mechanism rather than a catalogue and a consumer's own SVG is a first-class member with no new class; deprecated glyphs are shown apart from the active set and each carries its data-URI replacement in its own comment |