Skip to content

src/data/icon.css - one file

Icons

One grid, one stroke weight, no fill and no colour of its own. Every glyph below is drawn inside 24 by 24 at a stroke of 2 with round caps and joins, and every one of them is checked rendered at 16 as well as at 24, because that is the size where a shape either holds or turns to mud.

The set is authored as data in scripts/lib/icons.ts and this page is generated from it, so a corrected curve lands here, in docs/icons.md and in components.json at the same build. There is no second copy of a shape anywhere in the repository, which is the only arrangement under which this many of them stay one set.

The tiles below copy their markup to the clipboard, and the filter narrows the set by name. Both need script. With scripting off every glyph still renders, the whole set is still on the page, and the markup is in the panel under the first example.

48 glyphs

Each tile draws its glyph once, at 1.5rem, which is the 24 unit grid at its authored size. Press a tile and its inline SVG goes to the clipboard, flush left and ready to paste. The mark on the trailing edge turns into a tick when the copy lands; the word does not change, because a label that grows resizes the button under a pointer that is already on it. The same shapes at 1rem, the size where a curve either holds or turns to mud, are in Sizes below.

Inline SVG, and one class

An icon is markup, not an asset. There is no sprite to fetch, no font to load and no request to make: the shape is in the document and the file that paints it is one stylesheet the page has already linked. That is also what makes it take currentColor, inherit the theme and survive forced colors without a second copy for a dark background.

src/data/icon.css

Sent and read

Show codeHide code

aria-hidden="true" and focusable="false" are on every glyph in the set and they are not decoration. The first keeps a shape out of the accessibility tree, where it would otherwise be announced as an unnamed graphic beside the word it is illustrating. The second takes it out of the tab order, which older engines put SVG in by default. A glyph that is the only content of a control is a different case, and it is the last section on this page.

Five sizes, and the stroke moves with them

The steps are the type scale's, so an icon beside a word matches the word rather than a grid nobody can see. The stroke is lifted at the small end and dropped at the large end, because optical weight is not linear: a 2 unit stroke reads as a smudge at 14px and as a hair at 32px. It is unitless, so it is in the viewBox's own units and scales with the box.

bs-icon--xs to bs-icon--xl
Show codeHide code

Between the steps, set --bs-icon-size and --bs-icon-stroke yourself. A component that wants one size for every icon inside it sets the property on itself rather than on each glyph, which is what src/app/control-fab.css does.

Waiting

The spinner is one shape with a gap in it, and the gap is what makes the rotation visible. It is the only glyph in the set that carries an attribute of its own, and it stops under prefers-reduced-motion, where what is left is a ring with a notch rather than a blank.

bs-icon--spin

Checking the connection

Show codeHide code

Four inks, and none of them alone

A glyph inherits currentColor and needs nothing else. The four modifiers exist for the cases where the icon is carrying a status of its own, and each names a theme-aware ink rather than the raw fill: raw --ok measures 1.82:1 on a light card, which is under the 3:1 a meaningful non-text element owes. Switch the theme with the control in the bar and watch all four hold.

--ok, --danger, --info, --muted
Passed Failed Skipped Queued
Show codeHide code

Every row above states its status in a word next to the glyph. That is the rule the set is built on rather than a nicety: colour alone fails SC 1.4.1, and it fails in the ordinary way too, because in forced colors all four resolve to one ink and the four rows become four identical lines. Each of the four shapes is different for the same reason.

The same shape as a CSS mask

Where the glyph belongs to the stylesheet rather than to the document - a marker on a generated box, a chevron a component draws for itself - the same shape is available as a percent-encoded data URI. Set it as --bs-icon-src on an element carrying bs-icon-mask and the box takes the coverage of the shape and the colour of --bs-icon-color.

bs-icon-mask

Export the report

Show codeHide code

The whole set in this form, one custom property per glyph, is in docs/icons.md, generated from the same module as the tiles above. The stroke inside the data URI is a literal black rather than currentColor: a mask has no colour, only alpha, so what is drawn there is discarded and only its coverage is kept, and an engine that failed to parse an unresolved currentColor would drop the glyph entirely.

When the glyph is the whole control

Every shape in the set is hidden from the accessibility tree, which is right while there is a word beside it and wrong the moment there is not. A control whose entire content is a glyph has no accessible name at all, and the fix is on the control rather than on the shape: name the button, leave the SVG hidden. Tab to the two below and listen to them; both are announced, and neither announces the picture.

aria-label on the control, never on the SVG
Show codeHide code

The name says what the control does to what: "Delete this run", not "Delete" and certainly not "Trash". A screen reader user listing the controls on a page gets each name out of its context, and a row of eleven buttons called Delete is a row of eleven identical buttons. The second control needs no attribute at all, because it already has a word.

The target is the other half of it, and it is the half every implementation gets wrong: a 16px glyph with 4px of padding is a 24px box, which is the SC 2.5.8 minimum and nothing over it. src/app/control-icon-button.css is the answer to that, and it is documented with the rest of the application layer on the Application page.