Three per cent
You pressed it. It animated. Nothing happened. Buttons in Vela, the design system I build, shrank three per cent when you pressed them. One line of CSS, copied from somewhere, never questioned. It was throwing clicks away.
.vela-btn:active { transform: scale(0.97); } — while this button is held down, draw it at 97% of its size. From the centre, over 120 milliseconds, the same coming back.
I put it in because I believed everybody did it. That turned out to be the first thing worth checking, and it is not true. Of the systems I read the source of, only Linear does this exact thing. Material UI changes elevation and spreads a ripple — it never changes size. GOV.UK nudges the button down into its own shadow.
Three systems, three different answers, and a convention I had assumed into existence.
So: ten candidates, built as one page that counts its own presses, scored against criteria written before any of them were examined.
| Candidate | What it does | Where it came from |
|---|---|---|
| Shrink from the centre | draws at 97%, pulling away from wherever you pressed | what Vela shipped |
| Shrink from the cursor | same shrink, pivoted on the pixel you pressed, so that pixel cannot move | — |
| Light under the cursor | a highlight blooms where you pressed; nothing moves | — |
| Light on the nearest edge | the closest edge brightens — knows which side, not which pixel | survives tiny buttons |
| Nudge away from the cursor | slides 1px in the opposite direction, hit area held | GOV.UK’s idea, made directional |
| Shrink plus light | shrink says pressed, bloom says here | — |
| Shrink, hit area held | today’s shrink, plus an invisible layer holding the original footprint | the honest option: that knowing where you pressed isn’t worth its cost |
| Shrink and darken | 97% and 2% darker, from the centre | Linear’s published CSS |
| Take something away | the press adds nothing — removes the hover colour, tightens the focus ring | — |
| Everything at once | cursor-anchored shrink + darkening + faster down than up | the synthesis |
Everything at once won at 85 out of 120; shrink from the cursor came second at 81 — three per cent apart, inside the margin where the method calls a winner unreliable. It was right to. The winner shipped nothing but one idea.
A button that shrinks from its centre pulls both edges inward while it is still held. On a wide button that is about a millimetre each side. Press near the edge and, mid-press, the button is no longer under your cursor — so the click goes to the page behind it.
Nothing looks wrong when this happens. The button still animates. You press it again and blame yourself.
Watch it. Two buttons, one press each, the same three per cent — the left from the centre, the right from the cursor. Motion slowed:
A recording of the two buttons being pressed: the centre-shrinking one records pressed 1, landed 0, lost 1; the cursor-pivoted one lands its press. Motion is suppressed here because you have asked for reduced motion.
It only happens to mouse users. Run the identical gesture as a touch and nothing is lost, because the browser captures the pointer the moment a touch begins, so everything after that goes to whatever was first touched, whatever it does next. The specification requires that for touch and pen. Not for a mouse.
The input everybody pictures when they imagine pressing a button is the one input this cannot happen to. The people on trackpads are the ones losing clicks.
The fix is unglamorous: an invisible layer holding the button’s original footprint for exactly as long as the press lasts. Pure CSS, so any web framework built on Vela’s stylesheet gets it, and it costs nothing — no colour, no timing, no JavaScript.
It is also not a new idea, which is the part worth knowing. GOV.UK’s button sits on a shadow and drops into it when pressed, and their stylesheet carries this, in a comment, years old:
Use a pseudo element to expand the click target area to include the button’s shadow as well, in case users try to click it.
Same class of problem, same shape of fix. What nobody seems to have written down is that it applies to a scale too — and that with a scale the exposure grows with the button, so the widest, most important buttons on a page are the ones losing presses.
The test suite could never have caught this. A scripted click presses and releases in the same instant, so the button never shrinks in between. The test that catches it has to press, wait, then release — and once written, it failed on a rule that had already shipped.
Linear darkens the button two per cent while it is held. I built that in and could not see it. The obvious reading is that two per cent is too subtle.
It is not a matter of degree. The CSS filter that does this dims everything the button paints, including its label. Figure and ground darken together, so the gap between them — the thing your eye actually reads — barely moves.
Turning it up cannot rescue it, so colour came out. Not the options that add light at a point, though — those work, and they came out for a different reason. Vela’s motion rules say motion here is feedback, never decoration, and a glow spreading under the cursor of a security console is decoration.
Apple’s Designing Fluid Interfaces argues that an interface should feel connected to the hand moving it. A button that shrinks from its centre retreats from your cursor wherever your cursor is. It answers that you pressed, never where.
Pivoting the shrink on the pressed pixel fixes that completely.
The button collapses toward you instead of away. It is the best-feeling thing in the set — and it needs to know where the cursor is, which needs JavaScript.
That left two honest positions: withhold the better press so every target behaves identically, or let each target do the best it can and make the seam explicit. I took the second.
The press now has two layers, and the lower one stands alone. In CSS the button scales from its centre with the invisible layer holding its footprint — correct everywhere, no script. Where React is present, the scale pivots on the pressed pixel instead. The props, the markup and the accessibility are identical either way: a target with only the CSS is not getting a degraded button, it is getting the same button without the part only a pointer can supply.
The promise in the porting guide had to change with it. It used to say a port “translates only the syntax”. It now says an enhancement is additive by definition, may not touch the props or the markup or what the CSS does alone, and that skipping it costs feel and never function. That sentence is enforced — a browser test strips the JavaScript out and asserts the press is still correct.
Material UI takes the same trade and does not document it. Its ripple is pixel-accurate when there is a cursor, and silently starts from the centre when the press came from a keyboard.
The original rule excluded keyboard presses. That is right as far as it goes — keyboard users do not want geometry moving under them. What nobody noticed is that it left Space and Enter with no feedback at all.
Not subtler feedback. None. A whole category of user pressing a button and getting nothing back, because a rule written to protect them had no second clause.
The answer came from take something away, which placed sixth of ten: remove the hover colour, pull the focus ring onto the button’s edge. No movement, no duration, so “nothing animates from the keyboard” still holds — and the keyboard stops being the only input that goes unacknowledged.
Four changes, built from the options that placed second, third and sixth, plus one idea borrowed from the winner. The winner itself shipped nothing else.
- A press no longer moves its own target — the invisible layer, plus a browser test that fails the moment it is removed.
- The press is faster going down than up — 70 milliseconds in, 180 out, where it was 120 both ways. Seventy is quicker than a blink and under the point where delay starts to read as lag, so the acknowledgement arrives with the press rather than just after it.
- A keyboard press is acknowledged.
- The press pivots on the cursor where one can be read — as a layer that is additive by contract, not a fork.
And auditing a three per cent animation found a bug in a completely different state. In dark mode, the moment your cursor was over the primary button — before any press — its background sat at 3.18 against its own white label, where the standard requires 4.5. The destructive button was at 4.08. Neither pairing was in the automated contrast checks, so the suite had never looked at a hovered button at all.
“Accessibility verified in CI” was true only of buttons sitting still. Every mouse press begins from hover.
Both are fixed, and four hover pairs are now checked in both themes.
Everything here is reproducible, which is the only reason it is worth reading. The candidates are one self-contained page — open it, widen the buttons, press near the edges, and watch the tallies. The decision record has the scoring and the three ways this measurement misleads, each of which cost me a wrong answer first. The system itself is here, as @omkarux/vela, with the measurements as browser tests in the repository.