Wolf-Rayet · design system

Only one thing on a screen gets to shout.

Wolf-Rayet is a design system built around one rule. In any part of a screen, only one thing is allowed to demand attention. Colour, wording and motion can all raise the volume, so all three use the same five levels. The rule is checked on every build rather than left to judgement.

Opens in a new tab at r136.dev.

r136.dev · Meter, the appearance example, running live
125
decision records, 117 accepted
65
components with running examples
10
foundations
27
checks in the harness
4
resolved themes, no partial ones
7
packages in the monorepo

Why it exists

The problem it solves

Every mature design system has tokens, components and documentation. None of them stops a screen filling up with things competing for the reader.

Attention is limited. If one thing on a screen stands out more, everything else stands out less. That is fine when one element is meant to be noticed, and a problem when six are.

Most systems handle this with guidance: use emphasis sparingly, reserve red for errors. Guidance holds until a deadline, a new person on the team, or a feature that seems important enough to be an exception. Then the screen fills up and nobody can tell what matters.

Wolf-Rayet uses a limit instead. Each part of a screen has a budget for attention, and the things that spend it have to stay inside it. The colour scale, the component tiers, the composition rules, the motion and the wording all follow from that.

This matters most on screens someone monitors. A person watching a model, a fleet of machines or a support queue needs one question answered quickly: what needs me now. If ten things are asking at once, the answer is buried.

A quiet screen is not a broken one. If nothing needs attention, nothing should be asking for it.

Three channels

Three ways a screen raises its voice

Most systems manage the first one loosely and leave the other two alone.

Channel What it spends Governed by
Light Colour and contrast against the background. The one most systems already try to control. Measured from the rendered screen
Language The words. A sentence can be as loud as a colour, and no colour token stops it. Tone levels and a content linter
Motion Movement. Peripheral vision is built to catch it, so an animation beats almost anything else on the screen. Motion tokens and a limit per level

A system that controls colour but lets anything animate has only moved the problem.

The levels

Five levels, shared by all three

A component's colour, wording and motion all sit at the same level. A level 4 alert cannot be worded like a level 2 label, and a level 1 element cannot pulse.

Level Name What it means What it may do
0 Substrate The background everything else is read against. No colour of its own. Labels only. No visible movement.
1 Ambient Visible and readable, asking for nothing. Close to the background. Plain wording. Fades in and out, nothing more.
2 Marked Stands out when you look for it. Distinct but not attention seeking. Names what it refers to. Moves when something changes, never on a loop.
3 Directed Draws the eye once you are looking at that part of the screen. The clear focus locally. Says what to do next. May move once, to arrive.
4 Demanded Takes your attention whether you wanted it or not. Limited to one. The most any region may use. Short, direct, and says what happens if it is ignored. The only level allowed to keep moving, and only while it still needs an answer.

They share one scale because a reader can turn motion off. When that happens, colour and wording have to carry the hierarchy on their own, which only works if all three were set to the same level to begin with.

The limit

One demand per region, a fixed number per screen

Level 4 is capped twice: at most one in any region, and at most a set number across a whole screen.

No component can enforce that by itself. Ten components, each reasonable in isolation, still add up to a screen where everything glows, because none of them knows what else is there. So the cap belongs to the region, and each region has one owner.

The rule is at most one, not exactly one. Exactly one would force every calm screen to invent something urgent, which is the same problem in reverse.

Decision record 0010 · clearing a demand

Marking an alert as seen is what frees the slot again. Without that step, one alert that has already been handled keeps the space the next problem needs.

The same applies in reverse. A screen where every reading has gone quiet because the data stopped arriving looks exactly like a screen where everything is fine. So a reading nobody can confirm is not allowed to hold the one slot, and closing a panel over a second alert does not make a screen pass.

The layers

Every component sits in one of five layers

Atomic design was right that a system needs tiers. It borrowed them from chemistry, where the boundaries are real, and they do not carry over.

Teams spent years arguing whether a search field is a molecule or an organism, and the answer never changed anything they built. Wolf-Rayet asks a different question: how much control does this have over attention. That has one answer per component.

Layer Authority Examples
Substrate Has no emphasis of its own. Sets the baseline the others are measured against. Surfaces, dividers, rules, the page background
Emitter Can use an allocation. Cannot hand one out. Badge, status dot, meter, button, tag, label, input
Arbiter Ranks its own children when more than one wants attention. Table row, queue item, card, list item, toolbar
Scope Holds the budget for an area and shares it out. Panel, sidebar, modal, drawer, page region
View Adds up its scopes and caps how many demands run at once. A whole screen, or a route

Control only flows downward. An emitter asks. Only an arbiter ranks. Only a scope allocates. Only a view decides between scopes. A component doing something above its layer fails the build.

The names are ordinary words on purpose. The system is named after a type of star and the site after a star cluster, and that is as far as the astronomy goes.

The engine

How the limit is measured

The measurement is the part everything else depends on, so it was built first, before any components existed.

For every region of a rendered screen, in every theme, the engine measures how far the pixels sit from that region's own background, in a perceptual colour space, across the area the region covers. It reports a number between zero and one. It also counts the level 4 elements and names them. The result is attached to the build as a record anyone can read.

The first formula was wrong

It measured how much brighter things were than the background, which only works when the background is dark. On the light themes everything sits below the background, every term came out as zero, and a busy region measured as empty. It could also be cheated: a test case declaring a bright background under dark content scored 0.00000 and passed.

Direction was dropped, because what matters is the distance from the background, not which side of it something sits on. The formula was replaced early, while replacing it was still a week of work.

Decision record 004 · the formula

It is now measured as distance from the background in either direction, rather than brightness above it.

It measures the interface, not the content

Avatars, photographs, charts, text people wrote and embedded media are the product's content, not the system's design. Measuring them makes the limit impossible to meet in any real product, and a limit nobody can meet is one nobody follows.

So components declare which parts of them hold content, and the engine skips those areas. That declaration comes from the same contracts that govern composition, so there is no separate list to keep up to date. The frame around the content still counts: its container, its border, and anything the system draws on top of it.

It runs on whole screens

A component on its own says nothing about whether a screen is over budget. So the engine runs against full layouts: realistic screens with several regions, including ones built to break the rules and expected to fail. The failing cases matter as much as the passing ones.

Enforcement

Where the rules are checked

Every style guide already says to use emphasis sparingly, and that guidance drifts. These rules are checked in four places instead.

  1. Types, while you write. A container declares what it accepts by layer and role rather than by component name, so the wrong thing is flagged as you type it.
  2. A lint rule, on save. Nesting, motion values and hand written durations.
  3. A warning in the browser, in development. For layouts assembled at run time, which types cannot see.
  4. The full harness, on every build. The measurement above, run over whole screens in all four themes, plus everything below.

Under those four sit 27 checks. Some measure pixels. Several exist because something in the repository was wrong in a way no ordinary test could see.

Registry coverage

Lists that quietly go out of date

Eight lists across the packages and apps have to name every component. When one falls behind, nothing breaks loudly. A missing stylesheet import just renders a component unstyled, which looks like a design decision rather than a missing line.

Self description

Files that name the wrong component

A component's tests are written by copying the previous component's, which is a reasonable way to build the twenty fifth from the twenty fourth. The copy brings the old name with it, into the one place someone looks to find out what a file is for. Every test passed while twenty one of those lines sat in the repository.

Status figures

Numbers that go stale

Every figure in the status document is derived from the thing it describes, and a figure that no longer matches fails. The written history above it is left alone, because a record of what was measured, and when, has to stay as it was.

How it is built

Seven packages, one token source

Tokens are written in the W3C Design Tokens format rather than a private one. Colour is stored as OKLCH values, not hex, so lightness can be adjusted directly instead of being worked back out of a hex code. The colour ramps are generated from curves rather than picked by hand.

Contrast is calculated during the build with APCA, and WCAG 2.2 ratios are reported alongside it for the conversations where a familiar number is needed. The budget lowers emphasis and the contrast floor is the point it cannot go below. Where the two disagree, contrast wins and the token fails the build until it is changed.

There are four tiers of token: primitive, semantic, component and computed. The last one is unusual. Regions state their limits as tokens, components state their intent as tokens, and the build checks the rendered result against both. Design and code read the same file, so neither can drift from the other.

All four themes resolve every token. No partial themes and no environment added later, because adding one afterwards is how token systems rot. Components are designed first in the night theme, where colours cannot be told apart, so shape, position and weight carry the meaning and colour only reinforces it.

The packages

What each one does

tokens The token source, the generators and the CSS that ships
core Framework independent primitives, so the system is not tied to React
react The React components
icons The icon set and its build
eslint-plugin The composition and motion rules
content-lint The wording rules that can be checked automatically
budget The measurement engine and the test screens it runs against

The wording is checked too

The content standard is a linter rather than a document. It checks what can be checked: banned words, sentence case, ranges written with the word to, title case in buttons, a limit on courtesy words, and whether a message has all its required parts.

Tone is handled with a tag on each string rather than by reading the English. The linter checks that tag against the component's level. A linter that tries to judge tone produces false positives, and one that cries wolf gets turned off.

The record

125 decisions, written down

Each one records the situation, the decision, the options turned down, and what it cost. 117 of the 125 are accepted.

The rejected options are the part worth reading. They are where the argument happened, and the only part that cannot be worked out from the code. Three of them show what the records are for.

Decision record 0050 · a component removed

One component was dropped. Everything that made it look like its own component turned out to be a rule about how an alert behaves. Once those rules were written down separately, what was left was a button.

Decision record 0068 · a rule kept, its reason replaced

Three nesting rules shared one justification that only applied to one of them. The rules stayed and the reasoning was rewritten, because a rule with a bad reason attached is one someone will eventually argue away.

Decision record 0073 · two components that looked identical

Two panels matched on all eight tests used to merge duplicate components, and both were kept. They are two different places on a screen, and position is not something those tests measure.

The last one shows where the checks stop. Six of those eight tests run automatically, but for one whole group of components they give the same answer every time, so the difference has to be argued in the contracts instead. Knowing where the automatic checks run out is part of running the system.

Open Wolf-Rayet

The landing page, the argument in full, 10 foundations, 65 components with working examples, and all 125 decision records. It runs at r136.dev.

Open Wolf-Rayet

Opens in a new tab.