Architect
Standards
ReferenceNormative

The design system

Two layers, and the one test that decides which a component belongs to — stated independently of any folder layout, because it holds in all of them.

Every codebase with an interface has two kinds of component, whatever the tree looks like:

LayerOwnsAnswers to
The design systemProduct-wide visual and interaction decisionsEveryone
Feature UIHow one feature presents its own domainThat feature

The layers are a dependency direction, not a folder. The design system knows nothing about any feature; features compose the design system. That is the only structural rule, and it holds whether the tree is feature-first, layered, a monorepo package, or a single directory.

The promotion test

The recurring question is not what is a component — it is when something moves up. Reuse count is the wrong answer: two features needing the same thing can be coincidence, and a decision used once can still be product-wide.

Three questions, all of which must pass:

1 — Does it express a decision, or a domain?

A status pill encodes a decision about how this product shows state: everywhere, consistently, forever. An invoice row encodes what an invoice is. Decisions go up even at one usage. Domain stays down even at ten.

2 — Can you name it without naming the domain?

StatusPill promotes. InvoiceStatusPill does not — the name is telling you it still belongs to invoices. This is the designer lens applied to a component: say the name out loud, and if a domain noun is load-bearing in it, you are looking at feature UI.

3 — Does promoting it drag anything up with it?

If moving the component means moving a domain type, an API call or a feature import alongside it, it is not ready. The shared layer may not import a feature — so either extract the decision and leave the domain behind, or leave it where it is.

A component that fails only the third question is usually two components: a presentational one that promotes, and a feature-owned wrapper that supplies the domain.

What never goes up

  • Anything that fetches. A component that knows an endpoint has a feature's knowledge in it.
  • Anything conditioned on which feature is calling. See the tells below.
  • A layout convention. Rhythm, spacing and hierarchy are rules, not components — trying to componentise them produces wrappers nobody wants. They belong in layout and spacing.
  • A one-off that happens to look reusable. Promotion is not a compliment.

Promotion has a price

A promoted component is an API with more than one consumer. Every later change has to consider callers you did not write, and the ones you cannot see from where you are standing. That cost is the reason the test has three questions instead of "is it used twice".

Left in a feature, a component is free to change. Promote it and you have taken on a maintenance obligation on behalf of everyone — so promote deliberately, and never as tidying.

The junk-drawer tells

The shared layer degrades quietly. Three signs, each meaning something was promoted that should not have been:

TellWhat it means
A variant prop enumerating feature namesThe component never stopped being feature UI; it just moved.
Props only one caller ever setsIt was promoted for one need and nobody else's.
A shared component importing from a featureThe dependency rule already broke — this is the version a test catches.

The last one is worth enforcing mechanically, like every other boundary rule — see enforce it with a test.

Where the vendored primitives sit

Copied primitives — shadcn is the set in use — are the bottom of the design system, not the whole of it. They are unopinionated by design: a button, a dialog, a popover.

The design system is those primitives plus the decisions we made on top: which variants exist, what a status looks like, how a destructive action announces itself, what an empty state is. That second part is the actual design system, and it is the part no library can ship you.

A codebase that has only the primitives has a component library and no design system, which is why every screen looks slightly different.

Not everything is a component

Some product-wide decisions are better expressed as tokens, as a documented convention, or as a lint rule. Reach for a component when there is behaviour or structure to share; reach for a token when there is a value; reach for the craft pages when what you actually have is a rule about composition.

Recording a deliberate exception

A design system bans things — gradients, frosted glass, colour outside status. Those bans are right most of the time, which is exactly why an intentional departure is indistinguishable from a mistake, and why the next person to touch the file will helpfully remove it.

Annotate the departure at the point of departure, saying three things: the rule being broken, by name; that it is deliberate and who asked; and the constraint that shaped the specific value.

{/*
 * Frosted fill and a rule beneath: content blurs as it passes under the bar, which
 * says it continues rather than stopping there. The veil keeps 80% opacity — without
 * `backdrop-filter` a browser that ignores it gets a near-solid band instead of a
 * pane you read two overlapping texts through.
 *
 * ⚠️ The design system proscribes frosted glass; this is a requested exception, not
 * an oversight.
 */}

The warning sign is load-bearing: it is what a reader scanning for why is this here actually finds.

Not in the design system document. A rule with a list of exceptions attached stops being a rule — the next reader finds the list and adds to it. The document keeps saying no; the one place that departs says why.

Not in a commit message alone. Commit messages are found by people already looking for history. The person about to delete the line is looking at the line.

Count them. One or two per product is a design system being applied. A dozen is a design system being ignored, and the fix is to change the rule rather than keep annotating around it.

On this page