---
title: "the card that tilts out from under you"
url: "https://sanyam.sh/blog/the-card-flinches-when-you-reach-its-edge"
date: "2026-09-20"
readTime: "4 min read"
description: "A tilting card buzzes at its edges. Hovering it moves the card out from under the pointer, so the hover drops and it comes back. Put the hit box outside the tilt."
---

# the card that tilts out from under you

Reach for the edge of a card that tilts under your pointer and it buzzes. It
looks like a rendering fault. It is not.

Hovering the card tilts it away from you. Tilted, it is no longer under your
pointer, so the hover drops and the card comes back. Now you are hovering it
again.

_An interactive demo runs here on the page._

Every hover in there is real and every leave is correct. Nothing fires that
should not.

Two cards below. Same tilt, same geometry. One of them has a fix in it.

_An interactive demo runs here on the page._

## it stops when you look at it

Park the pointer at the edge and hold it perfectly still. The buzzing stops.

The card tilts once, drops the hover, levels, and then nothing happens at all. A
browser is not required to re-run hit testing for a pointer that has not
moved,[^spec] so the edge comes back under you and no second event fires.

| t | `rotateY` | under the pointer |
| --- | --- | --- |
| 16ms | -7.14° | the stage |
| 214ms | -0.24° | the button, nothing fired |
| 1512ms | 0.00° | the button, nothing fired |

One enter and one leave across the whole 1.5 seconds, with the pointer parked
the entire time.

So inspecting it is what kills it. A hand is never quite still, and that jitter
is the only reason you get a buzz instead of a dead card.

## why the edge moves

The browser tests hover against the box it painted, which is the box after the
transform. A face turning away under a perspective also gets smaller, so the
edge you are aiming at pulls about eight pixels inward.[^eight]

You are still pointing at where the card was. The card is not there any more.

> Hovering it moves the thing you are hovering. That is the whole bug.

## the fix

Put the thing that catches the pointer on something that does not tilt.

_An interactive demo runs here on the page._

The card keeps its transform and stops taking the pointer at all. The button
goes beside it instead, stretched over the slot they both sit in, so the hit box
is the slot's box and never moves.

```tsx
// a child, so it tilts with the card and the hit box goes with it
<Card style={{ transform }}>
  <button className="absolute inset-0" />
</Card>

// a sibling, so the hit box is the slot's box and cannot move
<Slot>
  <Card style={{ transform }} className="pointer-events-none" />
  <button className="absolute inset-0" />
</Slot>
```

## in short

Anything that moves when you hover it can do this. Keep the thing that catches
the pointer still, and let the thing you can see move.

[^spec]: CSS does not say whether an element still matches `:hover` after it
moves out from under a pointer that did not itself move. Chrome waits for the
next pointer event.

[^eight]: Measured on `foil-card`, the lab this came out of: a 420px card at
8.94 degrees pulls its near edge in by 8.1px.
