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.
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.
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.
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.
-
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.
-
A lint rule, on save. Nesting, motion values and hand written durations.
-
A warning in the browser, in development. For layouts assembled at run time, which types cannot see.
-
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.
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.
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.
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.
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.
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.
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.
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.
Opens in a new tab.