Icons

The icon set is a brand decision, not a dependency. The system declares which roles exist, the brand draws them, and no component ever names an icon library.

import from ~/system/icons (contract) · ~/brand/icons (glyphs)

The icon set is a brand decision

This app draws its own icons and depends on no icon library. That is possible because the design system does not name one. The system declares which icon ROLES exist, in src/system/icons.ts, and the brand supplies the glyphs, in src/brand/icons.tsx. It is the same seam as color: the contract says which, the brand says what.

The design-system components import icons.next, never ChevronRight. So re-iconing this site means editing one file, and no component knows it happened.

Roles are named for their job, never for their picture. A brand that expresses "expand" with a plus sign is still a legal brand, and nothing downstream has to care.

House style

square caps, mitred joins, 2px

Cut from the same geometry as the hero's 4px rules and its X/lines grid. Lucide's rounded terminals fight that, which is the other reason these are drawn here.

The contract — 14 roles

expand

an accordion or select, opening

next

forward: breadcrumb separator, pagination

previous

back

ellipsis

a truncated range

check

a chosen item

selected

the radio's filled marker

close

dismiss a dialog, sheet, toast

trendUp

a metric rose

trendDown

a metric fell

trendFlat

a metric did not move

success

it worked

error

it failed

warning

proceed carefully

info

context, not a problem

icons.*

Every role the design system consumes. Miss one and the build fails: the registry is typed, so a missing icon is a compile error rather than a blank space in production.

Brand glyphs — not the contract

plus
arrowRight
user
settings
trash
inbox
search
mail
wallet
activity
layers
share
refresh
logout

glyphs.*

The extra pictures this project needs for its own UI and documentation. The design-system components never reach for these, which is why they are a separate export.

Sizes

size-4 · 16px · inline with text
size-5 · 20px · the default
size-6 · 24px
size-8 · 32px · empty states

<Icon icon={glyphs.search} size='xs|sm|md|lg'>

A closed set of four. Color is inherited through currentColor, so an icon follows the text token around it and never carries a color class of its own.

Color is inherited

currentColor

The same icon on four text tokens. No icon ever carries its own color class.

Inside a component

Button sizes its own icons

Button already sets the glyph size. The call site passes the icon and nothing else.

An icon-only control has no visible label, so it needs an aria-label. The glyph is not a name.

Rules

Do

Reach for a contract role when one fits. icons.next means "forward", whatever it is drawn as.
Use the Icon wrapper so the size comes from the scale, not from a one-off class.
Let color inherit. An icon takes the token of the text around it.
Give every icon-only control an aria-label naming the action.
Re-iconing the site is an edit to src/brand/icons.tsx and nothing else.

Don't

Never import an icon library into a component. The set is a brand decision and lives in one file.
Do not name a role for its picture. It is `expand`, not `chevronDown`.
Do not use an icon as the only carrier of meaning. Pair it with text or a label.
Do not reach for a bracketed one-off size. Pick a step, or add one to the scale.