Rules

Three ideas generate every rule here. If a convention is ever unclear, derive the answer from them rather than asking.

First principles

  1. 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.

  2. 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.

  3. 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:tokens

Size, 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-reviewer

At 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:tokens

No 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:tokens

Heading, 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.

review

A 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.

review

You 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.