All work
Vela · 2026

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.

01The rule I never questioned

.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.

CandidateWhat it doesWhere it came from
Shrink from the centredraws at 97%, pulling away from wherever you pressedwhat Vela shipped
Shrink from the cursorsame shrink, pivoted on the pixel you pressed, so that pixel cannot move—
Light under the cursora highlight blooms where you pressed; nothing moves—
Light on the nearest edgethe closest edge brightens — knows which side, not which pixelsurvives tiny buttons
Nudge away from the cursorslides 1px in the opposite direction, hit area heldGOV.UK’s idea, made directional
Shrink plus lightshrink says pressed, bloom says here—
Shrink, hit area heldtoday’s shrink, plus an invisible layer holding the original footprintthe honest option: that knowing where you pressed isn’t worth its cost
Shrink and darken97% and 2% darker, from the centreLinear’s published CSS
Take something awaythe press adds nothing — removes the hover colour, tightens the focus ring—
Everything at oncecursor-anchored shrink + darkening + faster down than upthe 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.

02It was throwing clicks away

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.

A pressed button shrinking away from a stationary cursor A dashed outline marks where the button sits at rest. When pressed it scales inward, and the strip between the outline and the shrunken button stops being the button. A cursor resting just inside the original edge ends up in that strip, so the click is delivered to the page behind. Measured on the prototype by pressing 2px inside the edge of a 320px button: 160 of 160 presses were lost in Chromium, and 143 of 160 in WebKit. On the shipped button, forced to 320px and pressed the same way in Chromium, 10 of 30 were lost, and none with the fix. The shrink is exaggerated four times to be visible; the real value is three per cent. The dashed outline is where the button sits at rest. Shrink exaggerated 4× — the real value is 3%. Revoke access your cursor never moves this strip stopped being the button so the click goes to the page behind it On the prototype, pressing 2px inside the edge of a 320px button:160 of 160 lost in Chromium (Chrome’s engine), 143 of 160 in WebKit (Safari’s).On the shipped button, the same press in Chromium: 10 of 30 lost, none with the fix.

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.

The counter under the left button reads pressed 1, landed 0, lost 1. Same shrink on the right, pivoted on the cursor: it lands. Recorded by a test in the repository, so it re-records itself whenever the press changes.

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.

03The darkening does nothing

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.

The same button at three levels of press darkening Three identical buttons at no darkening, two per cent and ten per cent. Contrast between label and background reads 6.08, 6.00 and 5.69 to one — falling as darkening rises, so a heavier press effect makes the label harder to read, not easier. Revoke access at rest 6.08 : 1 Revoke access 2% darker — Linear’s value 6.00 : 1 Revoke access 10% darker 5.69 : 1 Press it harder and the label gets less readable, not more. Those numbers are contrast — how far apart label and background sit. Accessibility rules want 4.5 or better.

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.

04It ignores where you pressed

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.

How much the press knows, against what it costs to deliver A two-by-three matrix. The top row is effects deliverable in pure CSS, reaching any web framework built on the stylesheet; the bottom needs JavaScript and reaches React only. Columns are how much the press knows: nothing, which side, or which pixel. Every option that knows where you pressed sits in the JavaScript row. The pure-CSS row is empty for both — no candidate and no shipped system occupies it. Knows nothing Knows which side Knows which pixel Pure CSS any web framework Needs JS React only Shrink from centre Shrink and darken Shrink, area held Take something away Linear · GOV.UK Nothing is here. Not one of ten candidates. Not one shipped system. — Light on nearest edge Nudge away Shrink from cursor Light under cursor Shrink plus light Everything at once Material UI's ripple Strong text: built for Vela. Faded: shipped systems, read from their source and published stylesheets.

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.

05The keyboard got nothing

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.

06What shipped, and what it turned up

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.

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.