Super Hover
A super tiny library that hit-tests hover every frame. Unlike native :hover, it keeps tracking whatever sits under your pointer while you scroll or when things move on screen.
Why use this?
Well, you probably shouldn't. While scrolling, browsers mostly skip updating :hover to prioritize other important rendering work, which in most cases is the desired behavior. But Super Hover recomputes a hover-like hit every frame, which opens up the possibility of some fun creative effects and interactions.
Installation
1pnpm add super-hover
Usage
Wrap the area you care about, then mark the things that should act hoverable.
The active element gets data-super-hover-active, which is usually enough if you only want styling.
Basic usage
1import { createSuperHover } from "super-hover";23const root = document.querySelector<HTMLElement>("#list")!;45const superHover = createSuperHover({ root });67// later8superHover.destroy();
HTML
1<ul id="list" class="space-y-1">2 <li3 data-super-hover4 class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"5 >6 Inbox7 </li>8 <li9 data-super-hover10 class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"11 >12 Projects13 </li>14 <li15 data-super-hover16 class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"17 >18 Settings19 </li>20</ul>
Events
If styling is not enough, you can run code when the active element changes. Super Hover dispatches three custom events:
superhoverenterwhen an element becomes activesuperhoverleavewhen an element stops being activesuperhovermovewhile an element is active
In TypeScript, listen for those events on the root. They bubble from the active element.
Images on the right are synced when the enter event fires.
Listening on the root
1import {2 createSuperHover,3 type SuperHoverEventDetail,4} from "super-hover";56const root = document.querySelector<HTMLElement>("#list")!;78root.addEventListener("superhoverenter", (event) => {9 const e = event as CustomEvent<SuperHoverEventDetail>;10 console.log("entered", e.detail.current);11});1213root.addEventListener("superhoverleave", (event) => {14 const e = event as CustomEvent<SuperHoverEventDetail>;15 console.log("left", e.detail.previous);16});1718const superHover = createSuperHover({ root });
superhovermove is on by default in the core library. If you do not need move events, pass moveEventType: false.
Event detail
The events are CustomEvents, so the useful data lives on event.detail.
For superhoverenter and superhoverleave, detail has:
xandy: the last pointer position, in viewport coordinatesprevious: the element that was active before this change, ornullcurrent: the element that is active after this change, ornull
For superhovermove, detail has:
xandy: the last pointer position, in viewport coordinatescurrent: the currently active element
previous and current are DOM elements. If you need a value from your item, read it from the element with dataset, id, or whatever you rendered there.
Reading event detail
1import type { SuperHoverEventDetail } from "super-hover";23root.addEventListener("superhoverenter", (event) => {4 const e = event as CustomEvent<SuperHoverEventDetail>;5 const { x, y, previous, current } = e.detail;67 console.log(x, y);8 console.log(previous?.id);9 console.log(current?.id);10});
How it works
The library is very simple and short, so please read the source code (or ask your agent) for the full details.
In short, Super Hover keeps track of the last pointer and when the pointer moves, the page scrolls, or the viewport changes, it schedules a hit-test with requestAnimationFrame. Multiple updates in the same frame are coalesced, so they only produce one hit-test.
On that frame, Super Hover calls elementFromPoint(x, y), finds the closest element matching [data-super-hover], and updates the active element.
If the active element changes, Super Hover removes data-super-hover-active from the old element, adds it to the new one, and dispatches the custom events.
What about content-visibility?
Optimizing heavy content with content-visibility: auto can also make native hover feel more responsive. It lets the browser skip rendering for content until it is needed, which can leave enough room for :hover to update more often while scrolling.
That said, what the browser schedules, and how it prioritizes those updates is a bit of a black box to me, and the result does not seem fully deterministic. Super Hover, on the other hand, recomputes the active element from the last pointer position every frame. Of course, if the page is overloaded enough to drop frames, Super Hover can drop frames too.
If you have better insight into how browsers prioritize this, please . I would genuinely love to hear it.
Accessibility
Super Hover can make the interface change quickly during scroll or animated layout changes, which can be jarring for people who are sensitive to motion.
If you use it in production, please respect reduced-motion preferences. Either disable it when it makes sense,or provide an equivalent opt-out.
Respect reduced motion
1import { createSuperHover } from "super-hover";23const root = document.querySelector<HTMLElement>("#list")!;4const media = window.matchMedia("(prefers-reduced-motion: reduce)");56const superHover = createSuperHover({7 root,8 enabled: !media.matches,9});1011media.addEventListener("change", (event) => {12 if (event.matches) {13 superHover.pause();14 } else {15 superHover.resume();16 }17});
API
SuperHoverOptions
createSuperHover registers listeners for pointer moves, scroll, and resize, then hit-tests on scheduled animation frames. Pass these fields as options. It returns a controller with pause(), resume(), refresh(), and destroy().
On each scheduled hit-test, the library calls elementFromPoint, walks ancestors with closest(selector) to pick the nearest matched element, and—when root is set—verifies that it lies inside root. selector decides which nodes may activate; root only limits where hits count—it does not automatically target every descendant.
Default: true
Starts the controller running. When false, it starts paused and waits for resume().
Default: ["mouse", "pen"]
Pointer types allowed to update the tracked pointer position. Touch is off by default so finger scrolling does not create hover state.
Default: whole document
Optional boundary: the matched element must lie inside this subtree; omit for the whole document. You can pass an iframe Document or an element inside a same-origin iframe. Does not make every descendant node a target; that is controlled by selector instead.
Default: [data-super-hover]
CSS selector passed to element.closest from the hit-tested node; defines which elements may activate. Independent of root, which only scopes where hits count.
Default: data-super-hover-active
Attribute toggled on the active matched element while active, then removed when inactive.
Default: superhoverenter
CustomEvent type dispatched on the matched element when it becomes active. event.detail includes x, y, previous, and current.
Default: superhoverleave
CustomEvent type dispatched on the matched element when it stops being active. event.detail includes x, y, previous, and current.
Default: superhovermove
CustomEvent type dispatched on each scheduled hit-test while an element is active. Set to false to disable move events.
SuperHoverController
Returned by createSuperHover.
Pauses hit-testing and clears the active element.
Resumes hit-testing and schedules a fresh hit-test.
Schedules a fresh hit-test without changing the paused or destroyed state.
Removes listeners, cancels pending animation frames, and clears the active element.
SuperHoverEventDetail
Used by superhoverenter and superhoverleave.
Last pointer x position in viewport coordinates.
Last pointer y position in viewport coordinates.
Element that was active before this change, or null.
Element that is active after this change, or null.
SuperHoverMoveEventDetail
Used by superhovermove.
Last pointer x position in viewport coordinates.
Last pointer y position in viewport coordinates.
Currently active element.