Icons
Lucide, and only Lucide. Every icon inherits currentColor — an icon is never given a colour of its own, it takes the colour of the text it sits with.
--icon-size-sm--icon-size-md--icon-size-lgStroke is --icon-stroke: 1.75, not Lucide's default 2 — it sits better against DM Sans at small sizes. Lucide writes width and height inline, so the icon-sm / icon-md / icon-lg utilities exist to override it at class level and keep size a token.
import { Megaphone } from 'lucide-react';
// size and stroke come from the class, never from props
<Megaphone className="icon-md" aria-hidden="true" />
// an icon-only control needs its own name
<button aria-label="Delete review"><Trash2 className="icon-md" aria-hidden /></button>Category icons — the closed gap
Names and definitions are taken from the shipping product, not invented here. Category icons are drawn in --color-primary, never in the tier colour: the icon says what, the bar says how good.
ScoreBar reserves an icon slot and the design system deliberately ships no icon set — the app passes its own. That left seven categories with no named icon anywhere. This is the mapping. Category icons are drawn in --color-primary, never in the tier colour: the icon says what, the bar says how good.
Rules
- An icon beside a label is
aria-hidden. The label is the accessible name. - An icon-only control carries its own
aria-label, and still meets the 44px target. - No filled icons, no duotone, no second family. If Lucide has no glyph for it, use a word.
- Never a decorative icon. If removing it loses nothing, remove it.
theme.css.