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

```vue
<script setup lang="ts">
    import { useSuperHover } from "super-hover/vue";

    const rootRef = useSuperHover();
    const items = ["Inbox", "Projects", "Settings"];
</script>

<template>
    <ul ref="rootRef" class="space-y-1">
      <li
        v-for="item in items"
        :key="item"
        data-super-hover
        class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"
      >
        {{ item }}
      </li>
    </ul>
</template>
```


## 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 Vue, `useSuperHover` 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**

```vue
<script setup lang="ts">
    import { useSuperHover } from "super-hover/vue";

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

    const items = ["Inbox", "Projects", "Settings"];
</script>

<template>
    <ul ref="rootRef" class="space-y-1">
      <li
        v-for="item in items"
        :key="item"
        data-super-hover
        class="rounded-md px-3 py-2 data-[super-hover-active]:bg-neutral-100"
      >
        {{ item }}
      </li>
    </ul>
</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 `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**

```vue
<script setup lang="ts">
    import { useSuperHover } from "super-hover/vue";

    const rootRef = useSuperHover({
      onEnter(event) {
        const { x, y, previous, current } = event.detail;
        console.log(x, y);
        console.log(previous?.id);
        console.log(current?.id);
      },
    });
</script>

<template>
    <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>
</template>
```


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

```vue
<script setup lang="ts">
    import { computed, onMounted, onUnmounted, ref } from "vue";
    import { useSuperHover } from "super-hover/vue";
    import { useReducedMotion } from 'motion-v'

    const shouldReduceMotion = useReducedMotion()

    const rootRef = useSuperHover(
      computed(() => ({
        enabled: !shouldReduceMotion.value,
      })),
    );
</script>

<template>
    <ul ref="rootRef"><!-- ... --></ul>
</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.



| 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. 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. |
| enterEventType | string | superhoverenter | `CustomEvent` type dispatched on the matched element when it becomes active. `event.detail` includes `x`, `y`, `previous`, and `current`. |
| 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(options)` from `super-hover/vue`. Bind the returned ref to your root element with `ref="rootRef"` or your chosen name.



| Property | Type | Default | Description |
| --- | --- | --- | --- |
| enabled | boolean | true | When `false`, the Vue 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. 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. |
| enterEventType | string | superhoverenter | `CustomEvent` type dispatched on the matched element when it becomes active. |
| 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 Vue 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. |
