Wolf-Rayet · design system

A design system that limits what grabs attention.

Wolf-Rayet is built around one rule: in any part of a screen, only one thing may demand attention. Color, wording and motion share the same five levels, and every build checks the rule.

Both open in a new tab. The site is at r136.dev and the file is in Figma.

r136.dev · the Meter component, running live
143
decision records, 136 accepted
65
components with live examples
12
foundations
29
automated checks
4
themes, each one complete
7
code packages

Why it exists

The problem

Most design systems have color palettes, components and docs. None of them stops a screen filling up with things fighting for your eye.

Attention is limited. When one thing stands out more, everything else stands out less. That is fine for one thing, and a problem for six.

Most systems rely on guidelines like “use emphasis sparingly”. Those hold until a deadline, a new team member, or a feature that feels important enough to break them.

Wolf-Rayet sets a hard limit instead. Each part of a screen gets a fixed attention budget, and everything in it has to fit. The colors, components, layout rules, motion and wording all follow from that.

This matters most on screens people monitor, like a model, a fleet of machines or a support queue. They need to see what needs them now, and fast.

If nothing needs attention, nothing should ask for it.

Three channels

Color, words and motion

Most systems loosely manage color and leave words and motion alone.

ChannelWhat it usesHow it is checked
LightColor and contrast against the background.Measured on the rendered screen
LanguageThe words. A sentence can be as loud as a bright color.Tone levels and a wording checker (content linter)
MotionMovement. The eye catches it before almost anything else.Set motion values, checked in the stylesheets

Limiting color while letting anything animate does not fix the problem.

The levels

Five levels for all three

A component's color, wording and motion always sit at the same level. A level 4 alert can't be worded like a level 2 label, and a level 1 element can't pulse.

LevelNameMeaningAllowed
0SubstrateThe background everything sits on.No color of its own. Labels only. No movement.
1AmbientVisible, but asks for nothing.Close to the background. Plain wording. Fades in and out only.
2MarkedEasy to find when you look for it.Distinct but not pushy. Names what it refers to. Moves when something changes, never on a loop.
3DirectedCatches your eye within its part of the screen.The main focus in that area. Says what to do next. Can move once, when it appears.
4DemandedGrabs your attention whether you want it or not. Only one allowed.The most any region can use. Short and direct, and says what happens if you ignore it. The only level that can keep moving, and only until it is answered.

They share one scale because people can turn motion off. Then color and wording have to show what matters on their own, which only works if all three were set to the same level.

The limit

One demand per region, a set number per screen

Level 4 has two caps: one per region, and a set number per screen.

A single component can't enforce that. Ten components can each look reasonable and still add up to a screen where everything glows. So the cap belongs to the region, and each region has one owner.

The rule is at most one, not exactly one. Otherwise every calm screen would have to invent something urgent.

Decision record 0010 · clearing a demand

Marking an alert as seen frees its slot. Otherwise an alert that is already handled blocks the next one.

Quiet can mislead too. A screen that went quiet because the data stopped looks just like one where everything is fine. So a reading nobody can confirm can't hold the slot, and hiding a second alert behind a panel does not make a screen pass.

The layers

Five layers of components

Atomic design was right that a system needs tiers. But its chemistry tiers, atoms, molecules and organisms, don't fit interfaces.

Teams argued for years about whether a search field is a molecule or an organism, and it never changed what they built. Wolf-Rayet sorts components by one question: how much control does this have over attention? Each component has one answer.

LayerWhat it can doExamples
SubstrateNo emphasis of its own. It is the baseline everything else is measured against.Surfaces, dividers, rules, the page background
EmitterCan spend the attention it is given. Can't give any out.Badge, status dot, meter, button, tag, label, input
ArbiterRanks its own children when more than one wants attention.Table row, queue item, card, list item, toolbar
ScopeHolds the attention budget for an area and shares it out.Panel, sidebar, modal, drawer, page region
ViewAdds up its scopes and caps how many level 4s run at once.A whole screen, or a route

Control only flows down. An emitter asks, an arbiter ranks, a scope hands out budget, and a view decides between scopes. A component that acts above its layer fails the build.

The names are plain words on purpose. The system is named after a type of star and the site after a star cluster, and that is where the astronomy ends.

The engine

How the limit is measured

Everything depends on the measurement, so it was built first, before any components.

For each region of a rendered screen, in every theme, the engine measures how far the pixels differ from that region's background. It uses a color space that matches how people see. It gives a score from zero to one, and counts and names the level 4 elements. The result is saved with each build for anyone to read.

The first formula was wrong

It measured how much brighter things were than the background, which only works on dark backgrounds. On the light themes every value came out as zero, so a busy region looked empty. It could also be gamed: a test that claimed a bright background under dark content scored 0.00000 and passed.

The fix was to measure distance from the background in either direction. It was replaced early, while that was still only a week of work.

Decision record 0004 · the formula

Measure distance from the background in either direction, not brightness above it.

It measures the interface, not the content

Photos, avatars, charts, user text and embedded media are the product's content, not the system's design. Counting them would make the limit impossible to meet in a real product.

So components mark which parts hold content, and the engine skips them. Those marks come from the same contracts (the rules for what can nest where), so there is no extra list to maintain. The frame around the content still counts: its container, its border, and anything drawn on top.

It runs on whole screens

One component can't tell you if a screen is over budget. So the engine runs on full, realistic screens with several regions, including ones built to break the rules. Those have to fail, and that matters as much as the passing ones.

Enforcement

Where the rules are checked

Style guides say to use emphasis sparingly, and people drift. Here the rules are checked in four places.

  1. Types, while you write. A container states what it accepts by layer and role, not by component name, so the wrong thing is flagged as you type.
  2. A lint rule, on save. Catches bad nesting, motion values and hand written durations.
  3. A browser warning, during development. For layouts built at run time, which types can't see.
  4. The full test harness, on every build. The measurement above, run on whole screens in all four themes, plus the checks below.

Under those sit 29 checks. Some measure pixels. Several exist because something went wrong that no normal test could catch.

Registry coverage

Lists that fall out of date

Eight lists across the packages and apps must name every component. When one falls behind, nothing visibly breaks. A missing stylesheet import just leaves a component unstyled, which looks like a design choice.

Self description

Files that name the wrong component

New component tests start as a copy of the previous one's, and the copy brings the old name along. That name sits in the one place people look to see what a file is for. Every test passed while twenty one of those lines were in the repository.

Status figures

Numbers that go stale

Every number in the status document is worked out from the thing it describes, and a number that no longer matches fails. The written history above it stays as it was, because a record of past measurements should not change.

How it is built

Seven packages, one token source

Tokens, the named values for color, size and motion, are written in the W3C Design Tokens format, not a private one. Colors are stored as OKLCH, a format where lightness can be adjusted directly, instead of hex. Color ramps are generated from curves, not picked by hand.

Contrast is calculated during the build with APCA, a newer contrast method, and WCAG 2.2 ratios are reported alongside for people who expect them. The budget pushes emphasis down, and the contrast minimum is the floor. When they clash, contrast wins and the build fails until the token is fixed.

Tokens come in four tiers: primitive, semantic, component and computed. The computed tier is the unusual one. Regions set their limits as tokens, components set their intent as tokens, and the build checks the rendered screen against both. Design and code read the same file, so they can't drift apart.

All four themes define every token. No partial themes and no themes added later, because that is how token systems decay. Components are designed first in the night theme, where colors are hard to tell apart, so shape, position and weight carry the meaning and color only backs it up.

The packages

What each one does

tokensThe token source, the generators and the CSS that ships
coreFramework independent building blocks, so the system is not tied to React
reactThe React components
iconsThe icon set and its build
eslint-pluginThe nesting and motion rules
content-lintThe wording rules that can be checked automatically
budgetThe measurement engine and its test screens

The wording is checked too

The content standard is a linter, not a document. It checks what can be checked: banned words, sentence case, ranges written with “to”, title case in buttons, too many courtesy words, and whether a message has all its required parts.

Tone is set by a tag on each string, not by reading the English. The linter checks the tag against the component's level. Linters that try to judge tone raise false alarms, and people switch them off.

The record

143 decisions, written down

Each one records the situation, the decision, the options turned down and the cost. 136 of the 143 are accepted.

The rejected options are the useful part. That is where the argument happened, and the only part you can't work out from the code. Three examples.

Decision record 0050 · a component removed

A component was dropped. Everything that made it seem separate turned out to be rules about how alerts behave. Once those rules were written down, what was left was a button.

Decision record 0068 · a rule kept, its reason replaced

Three nesting rules shared one reason that only fit one of them. The rules stayed and the reasons were rewritten, because a rule with a bad reason gets argued away eventually.

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 sit in different places on a screen, and those tests don't measure position.

That last one shows where the checks stop. Six of the eight tests run automatically, but for one group of components they always give the same answer, so the difference has to be written into the contracts by hand. Knowing where automation ends is part of running the system.

Open Wolf-Rayet

See the full argument, 12 foundations, 65 components with working examples, and all 143 decision records at r136.dev.

Open Wolf-Rayet

Opens in a new tab.