<!-- Generated by scripts/generate-agent-markdown.ts — do not edit by hand. -->

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.

> This library and the live demos work best on desktop with a mouse or trackpad.



**Interactive demo (`scroll-list`):** Scrollable list comparing native vs Super Hover with hover-triggered styling



## 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

**Install**

```bash
pnpm add super-hover
npm i super-hover
yarn add super-hover
bun 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**

```typescript
import { createSuperHover } from "super-hover";

const root = document.querySelector<HTMLElement>("#list")!;

const superHover = createSuperHover({
  root,
  events: ["enter", "leave"],
});

// later
superHover.destroy();
```


**HTML**

```html
<ul id="list" class="space-y-1">
    <li
      data-super-hover
      class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"
    >
      Inbox
    </li>
    <li
      data-super-hover
      class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"
    >
      Projects
    </li>
    <li
      data-super-hover
      class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"
    >
      Settings
    </li>
</ul>
```


## Events

If styling is not enough, you can run code when the active element changes. Super Hover can dispatch these custom events:

- `superhoverenter` when an element becomes active, when enabled
- `superhoverleave` when an element stops being active, when enabled
- `superhovermove` while an element is active, when enabled

In TypeScript, listen for those events on the root. They bubble from the active element.

**Interactive demo (`scroll-menu`):** [Images](https://www.are.na/hugo-von-hofsten/grafik-j9_shkfbj2e) on the right are synced when the enter event fires.



**Listening on the root**

```typescript
import {
    createSuperHover,
    type SuperHoverEventDetail,
} from "super-hover";

const root = document.querySelector<HTMLElement>("#list")!;

root.addEventListener("superhoverenter", (event) => {
    const e = event as CustomEvent<SuperHoverEventDetail>;
    console.log("entered", e.detail.current);
});

root.addEventListener("superhoverleave", (event) => {
    const e = event as CustomEvent<SuperHoverEventDetail>;
    console.log("left", e.detail.previous);
});

const superHover = createSuperHover({
  root,
  events: ["enter", "leave"],
});
```


All custom events are off by default in the core library. If you need events, pass `events: ["enter", "leave"]`; add `"move"` for high-frequency move events. Event type options only customize the event names.

### Event detail

The events are `CustomEvent`s, so the useful data lives on `event.detail`.

For `superhoverenter` and `superhoverleave`, `detail` has:

- `x` and `y`: the last pointer position, in viewport coordinates
- `previous`: the element that was active before this change, or `null`
- `current`: the element that is active after this change, or `null`

For `superhovermove`, `detail` has:

- `x` and `y`: the last pointer position, in viewport coordinates
- `current`: 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**

```typescript
import type { SuperHoverEventDetail } from "super-hover";

root.addEventListener("superhoverenter", (event) => {
    const e = event as CustomEvent<SuperHoverEventDetail>;
    const { x, y, previous, current } = e.detail;

    console.log(x, y);
    console.log(previous?.id);
    console.log(current?.id);
});
```


## How it works

The library is very simple and short, so please read the [source code](https://github.com/danielpetho/super-hover/blob/main/packages/super-hover/src/index.ts) (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](https://motion.dev/magazine/collision-detection-in-hover-detection). Basically, it checks whether your pointer swept through matching elements and briefly adds super-hover state to them too.

**Interactive demo (`swept-hit-test-grid`):** A scrollable two-dimensional grid comparing native hover, Super Hover, and Super Hover with `sweptHitTest`.



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 event handlers, 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](https://motion.dev/magazine/collision-detection-in-hover-detection)  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](https://web.dev/articles/content-visibility) 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 reach out (hi@danielpetho.com). 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**

```typescript
import { createSuperHover } from "super-hover";

const root = document.querySelector<HTMLElement>("#list")!;
const media = window.matchMedia("(prefers-reduced-motion: reduce)");

const superHover = createSuperHover({
    root,
    enabled: !media.matches,
});

media.addEventListener("change", (event) => {
    if (event.matches) {
      superHover.pause();
    } else {
      superHover.resume();
    }
});
```


## 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.



| Property | Type | Default | Description |
| --- | --- | --- | --- |
| enabled | boolean | true | Starts the controller running. When `false`, it starts paused and waits for `resume()`. |
| pointerTypes |  | `["mouse", "pen"]` | Pointer types allowed to update the tracked pointer position. Touch is off by default so finger scrolling does not create hover state. |
| disableWhilePointerDown | boolean | false | Clears hover state while an allowed pointer is pressed, such as during text selection, and resumes hit-testing after release. |
| root | Document, Element, or omit | 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. |
| selector | string | [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. |
| activeAttribute | string | data-super-hover-active | Attribute toggled on the active matched element while active, then removed when inactive. |
| events |  | false | Custom events to dispatch. Pass `true` for enter/leave, or an array like `["enter", "leave", "move"]`. |
| enterEventType | string | superhoverenter | Name for the enter custom event when `events` includes `"enter"`. `event.detail` includes `x`, `y`, `previous`, and `current`. |
| leaveEventType | string | superhoverleave | Name for the leave custom event when `events` includes `"leave"`. `event.detail` includes `x`, `y`, `previous`, and `current`. |
| moveEventType | string | superhovermove | Name for the move custom event when `events` includes `"move"`. |
| sweptHitTest | boolean | 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. |
| sweptHitTestMargin | number | 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. |



### SuperHoverController



Returned by `createSuperHover`.



| Property | Type | Default | Description |
| --- | --- | --- | --- |
| pause | () => void |  | Pauses hit-testing and clears the active element. |
| resume | () => void |  | Resumes hit-testing and schedules a fresh hit-test. |
| refresh | () => void |  | Schedules a fresh hit-test without changing the paused or destroyed state. |
| destroy | () => void |  | Removes listeners, cancels pending animation frames, and clears the active element. |



### SuperHoverEventDetail



Used by `superhoverenter` and `superhoverleave`.



| Property | Type | Default | Description |
| --- | --- | --- | --- |
| x | number |  | Last pointer x position in viewport coordinates. |
| y | number |  | Last pointer y position in viewport coordinates. |
| previous | Element \| null |  | Element that was active before this change, or `null`. |
| current | Element \| null |  | Element that is active after this change, or `null`. |



### SuperHoverMoveEventDetail



Used by `superhovermove`.



| Property | Type | Default | Description |
| --- | --- | --- | --- |
| x | number |  | Last pointer x position in viewport coordinates. |
| y | number |  | Last pointer y position in viewport coordinates. |
| current | Element |  | Currently active element. |
