npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@guillemservera/details-core

v0.2.3

Published

Framework-agnostic core of details: DOM behavior for proximity hover, keyboard navigation, indicators and text effects.

Readme

@guillemservera/details-core

Framework-agnostic core of details. Most apps should install a framework package instead, such as @guillemservera/vue-details.

Every behavior takes elements that are already rendered and returns an object with destroy(). Call it before the elements are removed or replaced. Nothing touches the DOM until you call a create* function, so modules are safe to import during SSR.

Only option fields typed MaybeGetter accept a plain value or a getter: resumeDistance and gapClick for proximity hover; loop, whileHovered, resumeDistance, count, focusTarget and currentIndex for arrow navigation; reducedMotion for the highlight indicator; and the reactive text-effect options. A getter returning undefined uses the default. Other options—including axis, ignore, target, from, motion and store—do not accept getters. scrollToIndex, isDisabled and onIndexChange are callbacks.

Highlight

import { createProximityHover } from '@guillemservera/details-core/proximity-hover'
import { createArrowNavigation } from '@guillemservera/details-core/arrow-navigation'
import { createHighlightIndicator } from '@guillemservera/details-core/highlight-indicator'

const hover = createProximityHover(list, { axis: 'y' })
const keys = createArrowNavigation(list, { loop: true })
const indicator = createHighlightIndicator(list, marker)

const unsubscribe = hover.store.subscribe(() => console.log(hover.store.highlighted, hover.store.source))
  • createProximityHover(container, { axis, resumeDistance, gapClick, ignore, store }) highlights the item under the pointer, or the nearest one in gaps. The element under the pointer decides first, in the pointer event itself, like native :hover; geometry decides gaps and padding in the next frame. Over content matching ignore (group labels, headings) it highlights nothing.
  • createArrowNavigation(container, { axis, loop, whileHovered, resumeDistance, count, isDisabled, scrollToIndex, focusTarget, currentIndex, onIndexChange, store }) moves the highlight with arrow keys, Home and End, and activates the highlighted item with Enter and Space.
  • createHighlightIndicator(container, indicator, { target, from, motion, reducedMotion, store }) springs an absolutely positioned indicator onto the highlighted item, or onto the target selector.
    • reducedMotion (a boolean or getter, read on every update) makes it jump instead of glide; fades still run. It defaults to prefers-reduced-motion: reduce, so an app with its own setting passes reducedMotion: () => settings.reduceMotion || undefined.
    • The glide runs as a Web Animation sampled from the spring, so the compositor animates it and the main thread only works when the target changes. Where Element.animate is missing (happy-dom, jsdom, very old browsers), it falls back automatically to writing the styles on every animation frame, following the same spring.

createProximityHover returns { store, destroy, remeasure }; createHighlightIndicator also returns freeze(). Both measure again when the container or an item resizes, when the DOM changes, and when a CSS animation or a geometry transition ends on the container or an ancestor and changed its size on screen (an enter zoom). Call remeasure() for anything else that moves geometry without a DOM mutation, instead of touching the DOM to wake the observers. Hidden indicators collapse their box after fading out; removing the target clears it immediately, so an invisible marker cannot retain scroll overflow.

Items carry data-highlight-item (ITEM_ATTR), plus data-index in virtualized lists. The highlighted item gets data-highlighted (HIGHLIGHT_ATTR), and the container gets data-keyboard-navigation (KEYBOARD_ATTR) while the keyboard owns the highlight. Keyboard navigation keeps focus on the container: give it tabindex="0" (React uses tabIndex={0}), and usually give items tabindex="-1" when the container owns focus. Arrow keys, Home and End move the highlight; Enter and Space call .click() on the highlighted item. With the default whileHovered, arrow keys also work while the pointer is over the container before it has focus.

For a search input, pass focusTarget: input (or a getter): arrow keys keep focus there and Enter activates the rendered highlighted row. Home, End and Space retain their text-editing behavior; IME composition is ignored. currentIndex and onIndexChange connect a consumer-owned index model. With virtual count, isDisabled(index) skips disabled rows even when they are unmounted; without it, only rendered disabled rows can be skipped. Put data-index on the actual data-highlight-item, not its wrapper, and use scrollToIndex to render an offscreen row.

Arrow navigation returns { store, destroy }. Behaviors on the same container element share one store by default. Pass store: createHighlightStore() to share the store identity when a container is replaced, but selection is not guaranteed to survive: detaching the last container resets the highlight and transient state, and removed DOM elements are never retained. A HighlightStore exposes highlighted, source and subscribe(listener), which returns an unsubscribe function.

Integration patterns

  • A host that owns its highlight (a combobox or a headless menu with its own active item): mirror its active item into the store with store.highlight(item, 'keyboard') and call store.suspendPointer() on every non-pointer move, so a resting mouse cannot take the highlight back until it travels resumeDistance. Report pointer highlights back from store.subscribe.
  • Custom marker: when the host writes data-highlighted itself, mirror the store into your own attribute from store.subscribe and pass target: '[data-my-marker]'. The keyboard nudge (below) still applies to it.
  • Keyboard reveal pinning: when the host scrolls its own list to reveal the next item, measure the scroll around it and set store.nudge = { item, dx, dy } (the scroll delta) before the highlight reaches the DOM: the indicator keeps its place on screen instead of travelling with the content. For a virtualized row that is not rendered yet, keep the delta until the row mounts and set the nudge then.
  • Freezing on close: call indicator.freeze() to stop observing and animating while retaining the current rendered paint, including a glide in progress. This is terminal for that instance: remeasure() does nothing afterward. Call destroy() after the exit animation to restore the original styles, and create a new instance for another opening. No computed-style copying is needed in the consumer.

Text

import { createRollingText } from '@guillemservera/details-core/rolling-text'

const motion = createRollingText(source, viewport, { duration: () => settings.duration })
source.textContent = 'Next phrase' // animates
settings.duration = 300
motion.update() // applies changed getter options

createRollingText, createMorphText and createRouletteText(source, viewport, options) animate text changes in source. They read prefers-reduced-motion on every change; an app with its own reduced-motion setting passes animated: () => !settings.reduceMotion. The viewport is an empty sibling that the effect owns and marks aria-hidden. The parent of both must be position: relative and inline-block. Each returns { update, destroy }. update() settles on the current text when a resolved option changed, and does nothing otherwise.

Platform

import { detectPlatform, isApplePlatform } from '@guillemservera/details-core/platform'
  • detectPlatform(nav?) returns 'mac' | 'ios' | 'windows' | 'linux' | 'android' | 'chromeos' | 'unknown': Client Hints first, user agent as fallback, iPadOS as ios, and 'unknown' on the server.
  • isApplePlatform(platform).

Input capabilities

import { createInputCapabilities } from '@guillemservera/details-core/input-capabilities'

createInputCapabilities() returns { getSnapshot, getServerSnapshot, subscribe }, compatible with React's useSyncExternalStore. The snapshot has canHover ((any-hover: hover)), hasFinePointer ((any-pointer: fine)), primaryPointerIsCoarse ((pointer: coarse)), hasTouch and isTouchFirst (a coarse primary pointer and no hover). It updates live, for example when a mouse is connected; media listeners only exist while something is subscribed.

Shortcuts

import { createShortcutKeydownHandler, formatShortcutLabel } from '@guillemservera/details-core/shortcuts'

Binding syntax: _ joins a combination (mod_k, ctrl_shift_p), - a sequence (g-d), a single character is literal (?), and an array lists alternatives.

  • mod (also cmd, command) is ⌘ on macOS and iOS and Ctrl elsewhere. meta is always the Meta key: ⌘, Win or Super. Also ctrl, alt (option) and shift.
  • createShortcutKeydownHandler(getBindings, options) returns { handleKeydown, resetSequence }. Bindings have keys, handler, enabled, priority and usingInput (true, or the name of a field). Options: platform, sequenceTimeoutMs (900), preventDefault, respectDefaultPrevented, shouldHandle.
  • matchesShortcutEvent(event, shortcut, { platform }) and isEditableTarget(target).
  • formatShortcutLabel(keys, { platform, orLabel, thenLabel }) gives ⌘K, Ctrl+K or G then D; formatShortcutTokens(...) gives structured tokens for rendering each key; formatShortcutKey(key) formats a single key (arrowup).