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.
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.
| Channel | What it uses | How it is checked |
|---|---|---|
| Light | Color and contrast against the background. | Measured on the rendered screen |
| Language | The words. A sentence can be as loud as a bright color. | Tone levels and a wording checker (content linter) |
| Motion | Movement. 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.
| Level | Name | Meaning | Allowed |
|---|---|---|---|
| 0 | Substrate | The background everything sits on. | No color of its own. Labels only. No movement. |
| 1 | Ambient | Visible, but asks for nothing. | Close to the background. Plain wording. Fades in and out only. |
| 2 | Marked | Easy to find when you look for it. | Distinct but not pushy. Names what it refers to. Moves when something changes, never on a loop. |
| 3 | Directed | Catches 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. |
| 4 | Demanded | Grabs 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.
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.
| Layer | What it can do | Examples |
|---|---|---|
| Substrate | No emphasis of its own. It is the baseline everything else is measured against. | Surfaces, dividers, rules, the page background |
| Emitter | Can spend the attention it is given. Can't give any 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 attention budget for an area and shares it out. | Panel, sidebar, modal, drawer, page region |
| View | Adds 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.
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.
-
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.
-
A lint rule, on save. Catches bad nesting, motion values and hand written durations.
-
A browser warning, during development. For layouts built at run time, which types can't see.
-
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.
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.
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.
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.
What each one does
| tokens | The token source, the generators and the CSS that ships |
|---|---|
| core | Framework independent building blocks, so the system is not tied to React |
| react | The React components |
| icons | The icon set and its build |
| eslint-plugin | The nesting and motion rules |
| content-lint | The wording rules that can be checked automatically |
| budget | The 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.
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.
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.
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.
Opens in a new tab.