Rules
Three ideas generate every rule here. If a convention is ever unclear, derive the answer from them rather than asking.
First principles
-
Closed sets, not open values.
Every design decision is a selection from a named, finite set. Raw values are the enemy: 13px, #3a3a3a, mt-[7px]. The thing this system bans everywhere is the arbitrary value.
-
One source of truth per decision.
Each decision lives in exactly one place. We do not keep a second representation that has to be kept in sync with the first, because it will drift. This is why the design-system components here are copied byte-identical rather than rewritten, and why the contract in src/system/theme.css is never edited locally.
-
Selection in context, definition in source.
A usage site may choose from what a component offers. It may never restyle. The moment you want something the component does not offer, that is a new variant or a new component.
Hard rules
No exceptions without a written justification. A genuine one-off needs a
token-lint-ignore: <reason> on the same line.
No arbitrary values.
lint:tokensSize, color, spacing, radius, shadow and z-index come from a named scale. Never text-[0.7rem], p-[13px], z-[60], a hex literal, or an inline hsl(). Need a step that does not exist? Add it to the scale. Do not reach for a bracket.
Select, do not restyle.
design-system-reviewerAt a call site you may pick a variant and pass layout classes (mb-4, w-full, ml-auto, a flex or grid role). A visual class on an imported component — color, font, rounded, shadow, border-color — means a variant is missing. Add it to the component.
No raw style props.
lint:tokensNo color, bgColor, fontWeight, borderRadius or fontSize props, and no inline style attribute carrying design. Add a named variant instead.
Typography goes through the component set.
lint:tokensHeading, Body, Label, Code, Prose, and the brand extensions Display and Eyebrow. Never a raw <h1>-<h6> in a page or a section, and never a bare text-* utility.
Contrast is membership, not advice.
reviewA foreground and background pair is only in the set if it clears WCAG AA. Disabled is the one carve-out: the rule is that interactive and enabled pairs must clear contrast.
Focus visibility is a hard rail.
reviewYou cannot override focus away. There is no legitimate upside to crossing that line.
Primitives: how long may a scale be?
Functional scales — may be long
Each step does measurable, perceptible work. Type, color ramps, spacing, breakpoints. Ten color steps is fine because each is distinguishable on a real surface.
Perceptual scales — must be short
Perceived as a quality, not read as a quantity. Radius, elevation, border width, opacity, blur, duration, z-index. The eye fuses adjacent steps, so the cap is what a person can actually resolve.
The test for adding a step to a perceptual scale: could a user perceive its absence, and is it tied to a real intent? If not, it is inventory pretending to be a decision.
When adding or changing a component
- Name variants by intent, never by appearance. variant="primary", not color="purple".
- Compose axes, do not fuse them. emphasis x density, not a "secondary-compact" blob.
- Define every interactive state: default, hover, active, focus-visible, disabled, and loading if it is async.
- A change in structure is a new component. A change in one property is a variant.
- The same UI built inline in two or more places gets promoted to a shared primitive.
- Add the style-guide entry in the same change. A component with no entry is an incomplete change.
Before you finish
pnpm lint:tokens # must pass — no arbitrary values
pnpm check # astro check
pnpm typecheck # tsc --noEmit
pnpm build For a fuller sweep (call-site overrides, appearance-named variants, duplication, raw headings), delegate to the design-system-reviewer agent in the bluemonkeymakes workspace.