基础
Icons
One stroke weight, three sizes, and a library that has stopped shipping brand marks.
Every icon in the package comes from lucide-react, drawn at 1.5 stroke. There is one exception and it is bounded by size rather than by taste: a 12px mark inside a filled box — a checkbox tick, a combobox chip’s close, a table’s sort caret — is set at 2 or 3, because 1.5 at twelve pixels thins below a hairline and the glyph stops reading as a glyph.
Sixteen pixels is the default and carries most of the system: a chevron, a close, a check, a caret. 14px is for a mark inside a control’s own padding, where 16 would crowd the label — a select’s indicator, a popover’s close, a combobox’s clear. 18px is for a leading mark that opens a row rather than sitting inside one: a nav item, an alert’s tone mark, the command palette’s search. 20px is the app shell’s menu toggle, and 24px is reserved for an EmptyState, where the icon is the only thing on the surface.
Those four sizes and that stroke are named: --ico-s, --ico-m, --ico-l and --ico-stroke. Read them as the specification, not as the mechanism — nothing in the package reads them. Every icon in src/components is written as size={16} strokeWidth={1.5} in TSX, because the size lands on the SVG as an attribute rather than as a style. So the tokens are what a review checks against, and the numbers are what you will find in the source. Do not go looking for the var() that wires them together; there isn’t one.
An icon aligns by flex, never by baseline. Button sets inline-flex items-center gap-(--control-gap) on every variant, so the glyph centres against its label and the gap comes off the density axis rather than off a margin written at the call site. Centring is right on a line of text and wrong against a block of it: Alert’s tone mark sits beside a title and a paragraph, so it takes mt-px shrink-0 instead. shrink-0 is on most icons in the package and it is not decoration — without it a flex row squashes the glyph before it wraps the label, and a 16px icon compressed to 11 is the artefact everyone sees and nobody can name.
An icon is decoration until proven otherwise, so aria-hidden is the default and nearly every icon in the package carries it. A chevron on an accordion, a tick in a checkbox, a tone mark on an alert whose words already say what went wrong — announcing any of those is the same sentence twice. The exceptions are the controls with no text at all. FloatingIconButton makes that the type’s problem: label is a required prop, so the code does not compile without it. Button does not — its JSDoc says an icon-only button REQUIRES aria-label and nothing checks it, so that is the one place a review still has to look. An icon-only control with no accessible name is the single most common way a design system ships something unusable.
BRAND MARKS ARE GONE, and this is the kind of thing that breaks a build on upgrade rather than at review. The version resolved in this repository is lucide-react 1.40.0. It still ships thousands of icons and not one of them is a brand: there is no Github export, and no Twitter, Slack, Figma, Gitlab, Linkedin, Youtube, Chrome, Codepen, Framer, Dribbble, Instagram or Facebook either. import { Github } from "lucide-react" is a compile error, not a missing glyph. This documentation site has already paid it — the octocat in its own masthead is a hand-drawn path in apps/docs/src/components/GithubMark.tsx, filled with currentColor so it still follows the button into dark mode. @misoto22/design depends on lucide-react at ^1.33.0 as a dependency rather than a peer, so an app importing lucide directly resolves its own copy against its own range, and the failure lands on its upgrade rather than on ours.
Not a size — a mark inside an 18px box: a checkbox tick, a chip’s close, a table’s sort caret.
Inside a control’s own padding, where 16 would crowd the label: a select indicator, a popover close.
The default, and most of what the package draws: a chevron, a close, a check, a caret.
A leading mark that opens a row rather than sitting inside one: a nav item, an alert’s tone, the palette’s search.
The app shell’s menu toggle.
Reserved for an EmptyState, where the icon is the only thing on the surface.
Stroke, at 12px
Checkbox draws its tick at 3. The rule is one weight, 1.5, and everything in the package that breaks it is a 12px mark like this one, where 1.5 thins below a hairline and the glyph stops reading as a glyph.
Icon tokens
The written-down version of the rule. No component reads these — the numbers are literals at each call site — so treat a token here as the value a review asserts against rather than as the value a component resolves.
| 样例 | Token | 值 | 备注 |
|---|---|---|---|
--ico-s | 14px | ─── Icons ─── one stroke weight, three sizes. | |
--ico-m | 16px | — | |
--ico-l | 20px | — | |
--ico-stroke | 1.5 | — |