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.
Pass superHover from super-hover/svelte to {@attach} on the list root. It is an attachment factory, pass options to superHover(...), and Svelte handles cleanup when the element is removed or options change. Attachments require Svelte 5.29 or newer.
Basic usage
1<script lang="ts">2 import { superHover } from "super-hover/svelte";34 const items = ["Inbox", "Projects", "Settings"];5</script>67<ul {@attach superHover()} class="space-y-1">8 {#each items as item (item)}9 <li10 data-super-hover11 class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"12 >13 {item}14 </li>15 {/each}16</ul>
Events
If styling is not enough, you can run code when the active element changes. Super Hover can dispatch these custom events:
superhoverenterwhen an element becomes active, when enabledsuperhoverleavewhen an element stops being active, when enabledsuperhovermovewhile an element is active, when enabled
In Svelte, pass onEnter, onLeave, and onMove to superHover.
Images on the right are synced when the enter event fires.
Event handlers
1<script lang="ts">2 import { superHover } from "super-hover/svelte";34 const items = ["Inbox", "Projects", "Settings"];5</script>67<ul8 {@attach superHover({9 onEnter(event) {10 console.log("entered", event.detail.current);11 },12 onMove(event) {13 console.log("move", event.detail.current);14 },15 onLeave(event) {16 console.log("left", event.detail.current);17 },18 })}19 class="space-y-1"20>21 {#each items as item (item)}22 <li23 data-super-hover24 class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"25 >26 {item}27 </li>28 {/each}29</ul>
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 lang="ts">2 import { superHover } from "super-hover/svelte";34 function handleEnter(event) {5 const { x, y, previous, current } = event.detail;6 console.log(x, y);7 console.log(previous?.id);8 console.log(current?.id);9 }10</script>1112<ul {@attach superHover({ onEnter: handleEnter })}>13 <li id="inbox" data-super-hover>Inbox</li>14 <li id="projects" data-super-hover>Projects</li>15 <li id="settings" data-super-hover>Settings</li>16</ul>
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 any enabled custom events.
Swept hit test
While Super Hover computes what is under the pointer on each frame, very large or fast movements can still skip over some elements. sweptHitTest handles that with a collision-detection algorithm based on Motion.dev's wonderful hover detection article. Basically, it checks whether your pointer swept through matching elements and briefly adds super-hover state to them too.
Enable sweptHitTest when the skipped elements matter: dense grids, paint-like trails, scrubbers, or flash effects where every crossed item should briefly react. Leave it off when you only care about the final hovered element.
The tradeoff is that Super Hover works on arbitrary DOM elements, so it has to query matching candidates and read real browser geometry with getBoundingClientRect. If you own all item geometry yourself, a custom implementation can usually be faster.
If you enable sweptHitTest, keep the root as narrow as possible and tune sweptHitTestMargin for your layout. Larger margins catch bigger pointer/scroll jumps but test more elements per frame; smaller margins can be faster but may miss elements that jump across the pointer between frames.
Performance
Super Hover is inherently more expensive than leaving native :hover alone. The browser can skip or delay hover updates during scroll when it needs to prioritize other rendering work; Super Hover opts into doing extra work by scheduling its own hit-test and updating hover-like state.
For the default mode, that work is relatively small: it calls elementFromPoint, walks up to the closest matching selector, and toggles an attribute when the active element changes. Swept hit-testing does more geometry work, so it is intentionally opt-in.
The visible cost also depends on what you do when hover changes. Toggling a background color on a small list is usually cheap. Running expensive JavaScript in onEnter, triggering application state updates for many components, or animating costly CSS properties can make the difference much more noticeable. Prefer cheap visual changes and GPU-friendly animations where possible; animating properties like box-shadow or layout-affecting dimensions can be more expensive than transforms or opacity.
Other approaches
Own the geometry yourself
If every crossed item matters and you control the layout, a an implementation covered in Motion's hover article may be the most performant. For example, if you know every item size, position, scroll offset, and invalidation rule, you can avoid DOM queries and geometry reads that Super Hover needs in order to work with arbitrary elements.
If you want a reusable hover-like behavior that works on real DOM nodes, with even arbitrary or dynamic layouts, Super Hover handles that for you, with the performance tradeoff that comes from measuring real DOM.
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 lang="ts">2 import { superHover } from "super-hover/svelte";3 import { prefersReducedMotion } from 'svelte/motion';4</script>56<ul {@attach superHover({ enabled: !prefersReducedMotion.current })}>7 <!-- ... -->8</ul>
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: false
Clears hover state while an allowed pointer is pressed, such as during text selection, and resumes hit-testing after release.
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: false
Custom events to dispatch. Pass true for enter/leave, or an array like ["enter", "leave", "move"].
Default: superhoverenter
Name for the enter custom event when events includes "enter". event.detail includes x, y, previous, and current.
Default: superhoverleave
Name for the leave custom event when events includes "leave". event.detail includes x, y, previous, and current.
Default: superhovermove
Name for the move custom event when events includes "move".
Default: false
Enables swept hit-testing for fast pointer movement and scroll-driven misses. When enabled, Super Hover checks whether the pointer path, or moving element geometry, crossed matching candidates between frames and briefly activates crossed elements in path order before the final target stays active.
Default: 320
Pixel radius around the pointer used to pre-filter swept-hit-test candidates in both axes. Larger values catch bigger jumps but test more elements per frame; smaller values can be faster but may miss elements that jump across the pointer between frames. Only used when sweptHitTest is true.
UseSuperHoverOptions
UseSuperHoverOptions for the superHover attachment factory from super-hover/svelte.
Because attachments are reactive, {@attach superHover({ enabled: !paused })} re-runs automatically when reactive values inside the options change.
Default: true
When false, Super Hover does not mount on the root.
Default: ["mouse", "pen"]
Pointer types allowed to update the tracked pointer position. Touch is off by default.
Default: false
Clears hover state while an allowed pointer is pressed, such as during text selection, and resumes hit-testing after release.
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: auto
Events to dispatch. If omitted, callbacks enable their matching events; no callbacks means attribute-only mode.
Default: superhoverenter
Name for the enter custom event.
Default: superhoverleave
Name for the leave custom event.
Default: superhovermove
Name for the move custom event.
Default: false
Enables swept hit-testing for fast pointer movement and scroll-driven misses. When enabled, Super Hover checks whether the pointer path, or moving element geometry, crossed matching candidates between frames and briefly activates crossed elements in path order before the final target stays active.
Default: 320
Pixel radius around the pointer used to pre-filter swept-hit-test candidates in both axes. Larger values catch bigger jumps but test more elements per frame; smaller values can be faster but may miss elements that jump across the pointer between frames. Only used when sweptHitTest is true.
Default: omit
Runs when an enter event bubbles within root. The helper only enables/listens for enter events when onEnter is passed. event.detail includes x, y, previous, and current.
Default: omit
Runs when a leave event bubbles within root. The helper only enables/listens for leave events when onLeave is passed. event.detail includes x, y, previous, and current.
Default: omit
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.