I'm going to show you an effect that you'll recognise immediately, perhaps without ever having paid it much attention.
Take any collection of elements that react to hover: a list of menu items, swatches in a colour picker, squares in a grid. Now quickly swipe your cursor across them:
In real life, your hand moves across your desk, or your finger across the screen, in a continuous, unbroken motion. But this isn't reflected in the example above. Here, lights in the path of motion are switched on seemingly at random. Move slower, and you'll see that every element lights up, and the faster you swipe, the more elements are skipped.
Now run your mouse over this version. Swipe it as fast as you like: every cell you cross lights up, with nothing skipped.
Honestly when I created this second example I couldn't stop playing with it. It is weird how responsive it feels, why doesn't it always work like this? By the end of this post you'll know why, and how to build this improved hover yourself.
Surprisingly, this is the exact same problem that video game engines encounter when deciding whether a car has crashed, or any other type of collision has taken place. As such, a solution to our skipped elements was invented decades ago.
Discrete vs continuous motion
CSS selectors like :hover , Motion events like onHoverStart , and JS events like pointerenter are all afflicted by this skipped element problem.
The reason being, pointer position is sampled discretely, rather than continuously. Streamed as a series of points, via events, to the browser and then by the browser to our code.
To illustrate, lets imagine a row of elements, with a pointer moving slowly across them. The pointer events always come in at the same rate, so with slower motion these events are closer together. Meaning that it's more likely at least one pointer event lands on each element, triggering its hover state:
... continue reading