busy-office-ui
Menu

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.

Add line
View code
<span class="bo-icon" aria-hidden="true"
  style="--bo-icon-src: url('data:image/svg+xml,<svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;%23000&quot; stroke-width=&quot;2&quot; stroke-linecap=&quot;round&quot;><path d=&quot;M12 5v14M5 12h14&quot;/></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 wantUseWhy
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

ClassRole
.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 currentColor via mask-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-label on the button/link).
  • Deliberately a small set, not a library — add your own with one --bo-icon-src line, or paste inline SVG with fill="currentColor"; both are first-class. Set the custom property, not mask-image directly: .bo-icon is in @layer bo-components, so a mask-image override 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
↑↓ move Enter open Esc close