Sent and read
Show codeHide code
src/data/icon.css - one file
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.
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.
No glyph is named that
Names are the thing rather than the use: the arrow on a card is
arrow-right, and the one that opens a list is
chevron-down. Try fewer letters.
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.
Sent and read
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.
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.
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.
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.
Checking the connection
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.
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.
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.
Export the report
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.
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.
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.