<!-- 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**

```tsx
import { useSuperHoverRef } from "super-hover/react";

export function Example() {
    const rootRef = useSuperHoverRef();
    const items = ["Inbox", "Projects", "Settings"];

    return (
      <ul ref={rootRef} className="space-y-1">
        {items.map((item) => (
          <li
            key={item}
            data-super-hover
            className="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"
          >
            {item}
          </li>
        ))}
      </ul>
    );
}
```


## Events

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

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

In React, `useSuperHoverRef` exposes these as `onEnter`, `onLeave`, and `onMove`.

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



**Event handlers**

```tsx
import { useSuperHoverRef } from "super-hover/react";

export function Example() {
    const rootRef = useSuperHoverRef({
      onEnter(event) {
        console.log("entered", event.detail.current);
      },
      onLeave(event) {
        console.log("left", event.detail.previous);
      },
    });

    const items = ["Inbox", "Projects", "Settings"];

    return (
      <ul ref={rootRef} className="space-y-1">
        {items.map((item) => (
          <li
            key={item}
            data-super-hover
            className="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"
          >
            {item}
          </li>
        ))}
      </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 `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**

```tsx
import { useSuperHoverRef } from "super-hover/react";

export function Example() {
    const rootRef = useSuperHoverRef({
      onEnter(event) {
        const { x, y, previous, current } = event.detail;   
        console.log(x, y);
        console.log(previous?.id);
        console.log(current?.dataset.superHoverIndex);
      },
    });

    return (
      <ul ref={rootRef}>
        <li id="inbox" data-super-hover>Inbox</li>
        <li id="projects" data-super-hover>Projects</li>
        <li id="settings" data-super-hover>Settings</li>
      </ul>
    );
}
```


## 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 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](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**

```tsx
import { useEffect, useState } from "react";
import { useSuperHoverRef } from "super-hover/react";
import { useReducedMotion } from "motion/react"

export function Example() {
    const prefersReducedMotion = useReducedMotion();

    const rootRef = useSuperHoverRef({
      enabled: !prefersReducedMotion,
    });

    return <ul ref={rootRef}>{/* ... */}</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.



| 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. |
| 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 (default `[data-super-hover]`). Independent of `root`, which only scopes where hits count. |
| activeAttribute | string | data-super-hover-active | Attribute toggled on the active matched element (empty string while active, removed otherwise). Use it for styling, e.g. `data-[super-hover-active]:…`. |
| enterEventType | string | superhoverenter | `CustomEvent` type dispatched on the matched element when it becomes active (`bubbles: true`). |
| leaveEventType | string | superhoverleave | `CustomEvent` type dispatched on the matched element when it stops being active. `event.detail` includes `x`, `y`, `previous`, and `current`. |
| moveEventType | string or false | 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(root, options)` and `useSuperHoverRef(options)` from `super-hover/react`. Pass the mounted root as the first argument to `useSuperHover`, or use the callback ref returned from `useSuperHoverRef`.



| Property | Type | Default | Description |
| --- | --- | --- | --- |
| enabled | boolean | true | When `false`, the React helper does not mount Super Hover on the root. |
| pointerTypes |  | `["mouse", "pen"]` | Pointer types allowed to update the tracked pointer position. Touch is off by default. |
| selector | string | [data-super-hover] | CSS selector passed to `element.closest` from the hit-tested node; defines which elements may activate (default `[data-super-hover]`). Independent of `root`, which only scopes where hits count. |
| activeAttribute | string | data-super-hover-active | Attribute toggled on the active matched element (empty string while active, removed otherwise). Use it for styling, e.g. `data-[super-hover-active]:…`. |
| enterEventType | string | superhoverenter | `CustomEvent` type dispatched on the matched element when it becomes active (`bubbles: true`). |
| leaveEventType | string | superhoverleave | `CustomEvent` type dispatched on the matched element when it stops being active. |
| moveEventType | string or false | auto | Custom move event name, or `false` to disable move events. If neither `onMove` nor `moveEventType` is passed, the React helper disables move events for you. |
| onEnter | (event: SuperHoverEnterEvent) => void | no-op | Runs when an enter event bubbles within `root`. `event.detail` includes `x`, `y`, `previous`, and `current`. |
| onLeave | (event: SuperHoverLeaveEvent) => void | no-op | Runs when a leave event bubbles within `root`. `event.detail` includes `x`, `y`, `previous`, and `current`. |
| onMove | (event: SuperHoverMoveEvent) => void | 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`.



| 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` and `onMove`.



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