Turning screen-printing limits into design tokens
This site's visual direction is Riso screen printing. What I actually built was the printing constraints as tokens, so retheming touched two files and no component changed.
The visual direction here is Riso — screen printing with two inks, coarse texture, edges that miss each other by a hairline. Not out of nostalgia. I picked it because printing is a naturally restrained medium: its limits are physical rather than aesthetic, so it does not drift into the gradient-plus-rounded-corners look every template eventually converges on.
The real work was not drawing that look. It was writing those limits down as tokens.
Printing constraints, one by one
| In the print shop | Here |
|---|---|
| One ink per pass | Two inks: paper, fluoro orange, ink blue. No third accent. |
| Cuts are straight | Every --radius-* is 0. Not one rounded corner. |
| Impressions are solid | --shadow-ink is always 0 blur. A blurred shadow means I drifted. |
| Ink lines swell | --border-width: 3px; hairlines --border-hair: 2px |
| Registration slips | --misreg: 4px for card shadows, --overprint: 2px for the title ghost |
| Halftone dots | @include mx.halftone(...), density from --halftone-size / --halftone-dot |
| Paper is not white | --c-bg is paper #f5f1e8 |
Everything in that right column lives in one file: src/styles/settings/_tokens.scss, plus three mixins in tools/_mixins.scss.
What the token layer looks like
Two maps, one mixin:
$light: (
'bg': #f5f1e8,
'surface': #fffdf7,
'accent': #ff4a17,
'ink-2': #1b2a6b,
// …
);
$dark: (
'bg': #101433,
'accent': #ff4a17,
'ink-2': #5fd3e8,
// …
);
@mixin palette($map) {
@each $name, $value in $map {
--c-#{$name}: #{$value};
}
}
The payoff is blunt: components say var(--c-accent) and have no idea which ink they are. Dark mode swaps the recipe and not one component changes. That is also why recolouring the scrollbar later meant adding two keys to two maps and nothing else.
Two rules that keep overprint from becoming noise
Misregistration is addictive. Once you have it, you want it everywhere. I fixed two rules for myself.
The ghost appears only on the page’s main heading (h1), and only in the other ink.
Same-colour ghosting (text-shadow in the text colour, 0 blur, offset 2px) is a trap I fell into: the two layers nearly coincide, fatten every stroke edge, and the whole line looks dirty, like a rendering bug. Only a different ink reads as “two passes that missed”. Repeated h2–h4 never ghost; h2 gets a solid ink bar on the left instead.
Halftone never goes under body text.
On the homepage it is a gradient screen down the right edge — dense at the edge, masked away toward the centre — and it never crosses under the copy. Type on a dot screen is unreadable no matter how good the layout is.
Neither rule is taste. Both are a floor on legibility. Decoration that eats the contrast of your own body text has stopped being decoration.
System chrome stays out of it
I recently changed the global scrollbar from a solid ink bar to silently hidden with a neutral black-grey. Same logic, continued:
Scrollbars, select arrows, date pickers — system chrome should not upstage the print. They read as extensions of the operating system; paint one fluoro orange and it stops being atmosphere and starts being interference. So the scrollbar uses its own pair of neutral tokens, not --c-accent.
A visual direction should govern look, not widget semantics. Mix the two and you get noise.
Why the layering earns its keep
The direction I rejected was light Bento cards — rounded corners, gradients, soft shadows. Not because it is ugly: because it has no constraints. Without them, every component adds its own little touch, and the whole site ends up sharing one vague softness.
Print does the opposite. It says up front: two inks, straight lines, solid impressions — then makes you decide inside that box. Once the limits are tokens:
- A new component: use
var(--c-*)andvar(--space-*)and it is correct by construction. - Recolour or re-weight: edit
_tokens.scss. - Check whether you drifted: search for
border-radiusandblur. A hit means you did.
A constraint is not a labour-saving device. It is what turns a pile of scattered decisions into a single decision.

Comments
…