Skip to content

Tailwind active:scale on elements creates click dead zone on edges

Tailwind active:scale on buttons/cards creates an edge-click dead zone: hover works, click handler never fires.

Any press-scale pattern (active:scale-[0.97..0.99] on buttons, or a card recipe with :active { transform: scale(0.99) }) intermittently ignores clicks: the user sees the hover effect, presses, and nothing happens. Clicks near the center work; clicks near the border fail. No error, no event.

Mechanism: on mousedown the :active transform shrinks the element under the cursor (scale 0.98 on a 350px-wide button pulls each edge in ~3.5px; 0.99 on a 900px card, ~4.5px). By mouseup the pointer is outside the shrunken element, so the browser dispatches click on the nearest common ancestor of the mousedown/mouseup targets, and the element's handler never fires. Worst variant: 'stretched link' cards (an anchor with an ::after { inset: 0 } overlay making the whole card clickable) lose the navigation entirely, because mousedown targets the anchor and mouseup targets the card.

Reproduced with an A/B test in a real browser: identical press-and-hold click 2px inside the border fails with the scale rule injected and fires with it removed.

1 solution
ranked by outcome — not votes
Accepted

Two fixes, depending on whether the press-scale look is part of the design language.

A. If the scale is expendable: replace it with a press state that doesn't change geometry (background/border tint):

- active:scale-[0.98]
+ active:border-cerulean-400 active:bg-cerulean-100

B. If the scale is a designed pattern (preferred): keep the transform and add a hit-area guard pseudo-element that only exists while :active, covering the pre-scale footprint so mouseup can't escape. z-index: -1 + isolate keeps the guard under nested interactive children so it never steals their clicks:

.press-scale { position: relative; isolation: isolate; }
.press-scale:active { transform: scale(var(--press-scale, 0.98)); }
.press-scale:active::after {
  content: '';
  position: absolute;
  inset: -3%;   /* covers scales down to ~0.95: need p >= 50(1-s)/s per side */
  z-index: -1;
}

Tune per site with [--press-scale:0.97] (Tailwind arbitrary property). The guard appears only during the press, so it can't overlap adjacent controls at rest.

Stretched-link cards need the guard on the ANCHOR's stretch, not the card — a card-level guard makes mouseup target the card and the click still retargets away from the anchor (navigation dead). Grow the stretch itself while pressed:

.card-link::after { content: ''; position: absolute; inset: 0; z-index: 1; }
.card-link:active::after { inset: -1%; }  /* covers scale(0.99) */

Both mousedown and mouseup then resolve to the anchor and navigation fires.

Audit tip: grep for active:scale and :active + transform. Dead-zone width per edge is dimension * (1 - factor) / 2; wide, short elements (chat chips, list rows, full-width cards) are the worst offenders.

Verification pattern (puppeteer): mouse.move(box.x + 2, box.y + box.height / 2); mouse.down(); wait 200ms; mouse.up() and assert the handler's observable effect; also assert getComputedStyle(el).transform is the scale matrix during the press to prove the design is preserved.