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
1<script setup lang="ts">2 import { useSuperHover } from "super-hover/vue";34 const rootRef = useSuperHover();5 const items = ["Inbox", "Projects", "Settings"];6</script>78<template>9 <ul ref="rootRef" class="space-y-1">10 <li11 v-for="item in items"12 :key="item"13 data-super-hover14 class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"15 >16 {{ item }}17 </li>18 </ul>19</template>
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 Vue, useSuperHover exposes these as onEnter, onLeave, and onMove.
Images on the right are synced when the enter event fires.
Event handlers
1<script setup lang="ts">2 import { useSuperHover } from "super-hover/vue";34 const rootRef = useSuperHover({5 onEnter(event) {6 console.log("entered", event.detail.current);7 },8 onLeave(event) {9 console.log("left", event.detail.previous);10 },11 });1213 const items = ["Inbox", "Projects", "Settings"];14</script>1516<template>17 <ul ref="rootRef" class="space-y-1">18 <li19 v-for="item in items"20 :key="item"21 data-super-hover22 class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"23 >24 {{ item }}25 </li>26 </ul>27</template>
onMove is off by default. If you need pointer coordinates for things like tooltips or previews, pass onMove and read event.detail.x / event.detail.y.
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
1<script setup lang="ts">2 import { useSuperHover } from "super-hover/vue";34 const rootRef = useSuperHover({5 onEnter(event) {6 const { x, y, previous, current } = event.detail;7 console.log(x, y);8 console.log(previous?.id);9 console.log(current?.id);10 },11 });12</script>1314<template>15 <ul ref="rootRef">16 <li id="inbox" data-super-hover>Inbox</li>17 <li id="projects" data-super-hover>Projects</li>18 <li id="settings" data-super-hover>Settings</li>19 </ul>20</template>
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
1<script setup lang="ts">2 import { computed, onMounted, onUnmounted, ref } from "vue";3 import { useSuperHover } from "super-hover/vue";4 import { useReducedMotion } from 'motion-v'56 const shouldReduceMotion = useReducedMotion()78 const rootRef = useSuperHover(9 computed(() => ({10 enabled: !shouldReduceMotion.value,11 })),12 );13</script>1415<template>16 <ul ref="rootRef"><!-- ... --></ul>17</template>
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.
UseSuperHoverOptions
UseSuperHoverOptions for useSuperHover(options) from super-hover/vue. Bind the returned ref to your root element with ref="rootRef" or your chosen name.
Default: true
When false, the Vue helper does not mount Super Hover on the root.
Default: ["mouse", "pen"]
Pointer types allowed to update the tracked pointer position. Touch is off by default.
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.
Default: superhoverleave
CustomEvent type dispatched on the matched element when it stops being active.
Default: auto
Custom move event name, or false to disable move events. If neither onMove nor moveEventType is passed, the Vue helper disables move events for you.
Default: no-op
Runs when an enter event bubbles within root. event.detail includes x, y, previous, and current.
Default: no-op
Runs when a leave event bubbles within root. event.detail includes x, y, previous, and current.
Default: off
Runs on move events while an element is active. The helper only listens for move events when onMove is passed.
SuperHoverEventDetail
Used by superhoverenter, superhoverleave, onEnter, and onLeave.
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 and onMove.
Last pointer x position in viewport coordinates.
Last pointer y position in viewport coordinates.
Currently active element.