Link
A dithered hairline at rest that develops into ink under the pointer.
This one is CSS transitions rather than the dot engine, so the bench’s clock doesn’t reach it.
I build for clients near and far. The pipeline behind Automating my 35mm film scanning pipeline started with a scanner I nearly broke, and the case for treating speed as design came from measuring this site on a throttled phone.
States
-
Rest
nothing set
The dithered hairline in the text’s own colour.
-
Developed
hover or :focus-visible
Held here by setting the two custom properties the hover sets (--u-ink, --u-grain) to 100%.
Anatomy
| At rest | a dithered hairline under the text: 2px of ink, 1px gap, at 52% (64% in the dark theme) |
|---|---|
| Developing | the ink builds in --duration-develop (140ms) on --ease-front, behind a sparser grain front that reaches the end in half that |
| Leaving | both recede together in --duration-retract (100ms) |
| Text | --ink at rest, --state while hovered or focused, over --duration-color (180ms) |
| Where | prose, the paths in the agents view’s text, the performance panel’s note |
| Not on | links that wrap an image, rows, buttons, the rail |
| Reduced motion | the underline still develops, at once |
Timings from the developing underline in global.css.
Decisions
-
A print coming up
The underline behaves like a print in the developer: a dithered hairline at rest, and ink building left to right behind a grain front, so the leading edge is ragged rather than a clean wipe.
-
Four backgrounds, no pseudo-elements
The layers are backgrounds on the link itself, so the effect survives a link that wraps onto a second line.
-
State, not decoration
Under reduced motion the underline still appears; it just doesn’t develop.
-
The rule, in CSS
Until Nov 2026 the ink took 320ms and the grain 170ms on curves of their own, and withdrew in 150ms, so a prose link and a text button, which is the prose link at control size, moved differently. It now keeps the rule’s times and ease from the tokens.