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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@versini/ui-hooks

v6.1.1

Published

[![npm version](https://img.shields.io/npm/v/@versini/ui-hooks?style=flat-square)](https://www.npmjs.com/package/@versini/ui-hooks) ![npm package minimized gzipped size](<https://img.shields.io/bundlejs/size/%40versini%2Fui-hooks?style=flat-square&label=s

Readme

@versini/ui-hooks

npm version npm package minimized gzipped size

A collection of useful React hooks built with TypeScript for UI component development.

This package provides reusable React hooks that are commonly needed when building UI components, including unique ID generation, click outside detection, keyboard shortcuts, local storage management, viewport tracking, and many other utilities.

Table of Contents

Features

  • 🎯 UI-Focused: Hooks specifically designed for UI component development
  • ♿ Accessibility: Hooks that enhance accessibility (unique IDs, focus management)
  • 🔧 TypeScript: Fully typed with comprehensive type definitions
  • 🌲 Tree-shakeable: Import only the hooks you need
  • ⚡ Performance: Optimized hooks with minimal overhead
  • 📱 Responsive: Hooks for viewport and visual viewport tracking
  • ⌨️ Keyboard: Hotkey and keyboard shortcut management
  • 💾 Storage: Local storage integration with React state

Installation

npm install @versini/ui-hooks

Note: While this package contains React hooks without styling, when used alongside the UI component packages it assumes TailwindCSS and the @versini/ui-styles plugin are configured. See the installation documentation for complete setup instructions.

Usage

Each hook is exported from its own subpath for optimal tree-shaking:

import { useUniqueId } from "@versini/ui-hooks/use-unique-id";
import { useClickOutside } from "@versini/ui-hooks/use-click-outside";
import { useHotkeys, getHotkeyHandler } from "@versini/ui-hooks/use-hotkeys";
import { useHaptic } from "@versini/ui-hooks/use-haptic";
import { useLocalStorage } from "@versini/ui-hooks/use-local-storage";
import { useViewportSize } from "@versini/ui-hooks/use-viewport-size";
import { useVisualViewportSize } from "@versini/ui-hooks/use-visual-viewport-size";
import { useInViewport } from "@versini/ui-hooks/use-in-viewport";
import { useResizeObserver } from "@versini/ui-hooks/use-resize-observer";
import { useInterval } from "@versini/ui-hooks/use-interval";
import { useIsMounted } from "@versini/ui-hooks/use-is-mounted";
import { useMergeRefs } from "@versini/ui-hooks/use-merge-refs";
import { useUncontrolled } from "@versini/ui-hooks/use-uncontrolled";

Available Hooks

Core Utility Hooks

  • useUniqueId - Generate unique IDs for accessibility
  • useIsMounted - Check if component is mounted
  • useMergeRefs - Merge multiple React refs
  • useUncontrolled - Manage controlled/uncontrolled state

Interaction Hooks

  • useClickOutside - Detect clicks outside an element
  • useHotkeys - Handle keyboard shortcuts and hotkeys
  • useHaptic - Provide haptic feedback for mobile devices

Storage Hooks

  • useLocalStorage - Sync state with localStorage

Viewport and Size Hooks

  • useViewportSize - Track browser viewport dimensions
  • useVisualViewportSize - Track visual viewport (mobile-friendly)
  • useInViewport - Detect if element is visible in viewport
  • useResizeObserver - Observe element size changes

Timer Hooks

  • useInterval - Manage intervals with start/stop controls

API Reference

useUniqueId

Generates a unique ID string for use in components.

const id = useUniqueId(prefix?: string): string

Parameters:

  • prefix (optional): String prefix for the generated ID

Returns: A unique ID string

useClickOutside

Triggers a callback when clicking outside the target element.

const ref = useClickOutside<T>(
  handler: () => void,
  events?: string[] | null,
  nodes?: (HTMLElement | null)[]
): RefObject<T>

Parameters:

  • handler: Function called when clicked outside
  • events: Array of events to listen to (default: ["mousedown", "touchstart"])
  • nodes: Array of additional nodes to check against

Returns: Ref to attach to the target element

useHotkeys

Handle keyboard shortcuts and hotkeys.

useHotkeys(
  hotkeys: HotkeyItem[],
  tagsToIgnore?: string[],
  triggerOnContentEditable?: boolean
): void

Parameters:

  • hotkeys: Array of [shortcut, handler, options?] tuples
  • tagsToIgnore: HTML tags to ignore (default: ["INPUT", "TEXTAREA", "SELECT"])
  • triggerOnContentEditable: Whether to trigger on contentEditable elements

useHaptic

Provide haptic feedback for mobile devices using the Vibration API or iOS switch element fallback.

const { haptic } = useHaptic(): { haptic: (count?: number) => void }

Parameters:

  • count (optional): Number of haptic pulses to trigger (default: 1)

Returns: Object with haptic function to trigger feedback

Example:

import { useHaptic } from "@versini/ui-hooks/use-haptic";

function HapticButton() {
  const { haptic } = useHaptic();

  return (
    <button onClick={() => haptic(1)}>Tap me (with haptic feedback)</button>
  );
}

Notes:

  • Uses navigator.vibrate when available
  • Falls back to iOS switch element trick for Safari on iOS
  • Haptic duration: 50ms per pulse
  • Interval between pulses: 120ms
  • Multiple pulses create a vibration pattern for better UX

useLocalStorage

Manage state synchronized with localStorage.

const [value, setValue, resetValue, removeValue] = useLocalStorage<T>({
  key: string,
  initialValue?: T
}): [T, (value: T) => void, () => void, () => void]

Parameters:

  • key: localStorage key
  • initialValue: Default value if not found in storage

Returns: Tuple of [value, setValue, resetValue, removeValue]

useViewportSize

Track browser viewport dimensions.

const { width, height } = useViewportSize(): { width: number, height: number }

Returns: Object with current viewport width and height

useVisualViewportSize

Track visual viewport dimensions (accounts for mobile keyboards, zoom).

const { width, height } = useVisualViewportSize(): { width: number, height: number }

Returns: Object with current visual viewport width and height

useInViewport

Detect if an element is visible in the viewport.

const { ref, inViewport } = useInViewport<T>(): { ref: RefCallback<T>, inViewport: boolean }

Returns: Object with ref to attach to element and visibility boolean

useResizeObserver

Observe element size changes using ResizeObserver API.

const [ref, rect] = useResizeObserver<T>(options?: ResizeObserverOptions): [RefObject<T>, ObserverRect]

Parameters:

  • options: ResizeObserver configuration options

Returns: Tuple of [ref, rect] where rect contains dimensions

useInterval

Manage intervals with start/stop controls.

const { start, stop, active } = useInterval(
  fn: () => void,
  interval: number
): { start: () => void, stop: () => void, active: boolean }

Parameters:

  • fn: Function to execute at each interval
  • interval: Interval time in milliseconds

Returns: Object with start/stop functions and active state

useIsMounted

Check if component is currently mounted.

const isMounted = useIsMounted(): () => boolean

Returns: Function that returns true if component is mounted

useMergeRefs

Merge multiple React refs into a single ref callback.

const mergedRef = useMergeRefs<T>(refs: Array<React.Ref<T>>): RefCallback<T>

Parameters:

  • refs: Array of refs to merge

Returns: Single ref callback that forwards to all provided refs

useUncontrolled

Manage controlled/uncontrolled component state patterns.

const [value, setValue, isControlled] = useUncontrolled<T>({
  value?: T,
  defaultValue?: T,
  finalValue?: T,
  onChange?: (value: T) => void,
  initialControlledDelay?: number
}): [T, (value: T) => void, boolean]

Parameters:

  • value: Controlled value
  • defaultValue: Initial uncontrolled value
  • finalValue: Fallback value when others are undefined
  • onChange: Change handler for controlled mode
  • initialControlledDelay: Delay before controlled mode activates

Returns: Tuple of [value, setValue, isControlled]

Comprehensive Examples

Accessible Form Field

import { useUniqueId } from "@versini/ui-hooks/use-unique-id";

function FormField({ label, helpText, error, ...props }) {
  const fieldId = useUniqueId("field");
  const helperId = useUniqueId("helper");
  const errorId = useUniqueId("error");

  return (
    <div>
      <label htmlFor={fieldId}>{label}</label>
      <input
        id={fieldId}
        aria-describedby={[helpText && helperId, error && errorId]
          .filter(Boolean)
          .join(" ")}
        aria-invalid={!!error}
        {...props}
      />
      {helpText && <div id={helperId}>{helpText}</div>}
      {error && (
        <div id={errorId} role="alert">
          {error}
        </div>
      )}
    </div>
  );
}

Modal with Click Outside and Hotkeys

import { useClickOutside } from "@versini/ui-hooks/use-click-outside";
import { useHotkeys } from "@versini/ui-hooks/use-hotkeys";
import { useUniqueId } from "@versini/ui-hooks/use-unique-id";

function Modal({ isOpen, onClose, title, children }) {
  const titleId = useUniqueId("modal-title");
  const descId = useUniqueId("modal-desc");
  const ref = useClickOutside(() => onClose());

  useHotkeys([["Escape", onClose]]);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay">
      <div
        ref={ref}
        role="dialog"
        aria-labelledby={titleId}
        aria-describedby={descId}
        className="modal"
      >
        <h2 id={titleId}>{title}</h2>
        <div id={descId}>{children}</div>
        <button onClick={onClose}>Close</button>
      </div>
    </div>
  );
}

Responsive Component with Viewport Tracking

import { useViewportSize } from "@versini/ui-hooks/use-viewport-size";
import { useVisualViewportSize } from "@versini/ui-hooks/use-visual-viewport-size";

function ResponsiveComponent() {
  const viewport = useViewportSize();
  const visualViewport = useVisualViewportSize();

  const isMobile = viewport.width < 768;
  const keyboardVisible = visualViewport.height < viewport.height;

  return (
    <div>
      <p>
        Viewport: {viewport.width}x{viewport.height}
      </p>
      <p>
        Visual Viewport: {visualViewport.width}x{visualViewport.height}
      </p>
      <p>Device: {isMobile ? "Mobile" : "Desktop"}</p>
      {keyboardVisible && <p>Virtual keyboard is visible</p>}
    </div>
  );
}

Auto-Save with Local Storage and Intervals

import { useLocalStorage } from "@versini/ui-hooks/use-local-storage";
import { useInterval } from "@versini/ui-hooks/use-interval";
import { useIsMounted } from "@versini/ui-hooks/use-is-mounted";

function AutoSaveEditor() {
  const [content, setContent] = useLocalStorage({
    key: "editor-content",
    initialValue: ""
  });
  const [lastSaved, setLastSaved] = useState<Date | null>(null);
  const isMounted = useIsMounted();

  const { start, stop, active } = useInterval(() => {
    if (isMounted() && content) {
      // Auto-save logic here
      setLastSaved(new Date());
    }
  }, 30000); // Auto-save every 30 seconds

  useEffect(() => {
    if (content) {
      start();
    } else {
      stop();
    }
  }, [content, start, stop]);

  return (
    <div>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Start typing..."
      />
      <div>
        Auto-save: {active ? "Active" : "Inactive"}
        {lastSaved && ` - Last saved: ${lastSaved.toLocaleTimeString()}`}
      </div>
    </div>
  );
}

Lazy Loading with Intersection Observer

import { useInViewport } from "@versini/ui-hooks/use-in-viewport";

function LazyImage({ src, alt, placeholder }) {
  const { ref, inViewport } = useInViewport();
  const [loaded, setLoaded] = useState(false);

  return (
    <div ref={ref}>
      {inViewport && !loaded && (
        <img src={placeholder} alt={alt} style={{ filter: "blur(5px)" }} />
      )}
      {inViewport && (
        <img
          src={src}
          alt={alt}
          onLoad={() => setLoaded(true)}
          style={{ display: loaded ? "block" : "none" }}
        />
      )}
    </div>
  );
}

Resizable Panel with Size Tracking

import { useResizeObserver } from "@versini/ui-hooks/use-resize-observer";

function ResizablePanel({ children }) {
  const [ref, rect] = useResizeObserver();

  return (
    <div
      ref={ref}
      style={{
        resize: "both",
        overflow: "auto",
        border: "1px solid #ccc",
        minWidth: 200,
        minHeight: 100
      }}
    >
      <div>
        Size: {Math.round(rect.width)}x{Math.round(rect.height)}
      </div>
      {children}
    </div>
  );
}

Haptic Feedback for Interactive UI

import { useHaptic } from "@versini/ui-hooks/use-haptic";

function InteractiveCounter() {
  const [count, setCount] = useState(0);
  const { haptic } = useHaptic();

  const increment = () => {
    setCount((c) => c + 1);
    haptic(1); // Single pulse
  };

  const decrement = () => {
    setCount((c) => c - 1);
    haptic(1); // Single pulse
  };

  const reset = () => {
    setCount(0);
    haptic(2); // Double pulse for emphasis
  };

  const celebrate = () => {
    haptic(3); // Triple pulse for celebration
  };

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
      {count >= 10 && <button onClick={celebrate}>🎉 Celebrate!</button>}
    </div>
  );
}

Advanced Controlled/Uncontrolled Input

import { useUncontrolled } from "@versini/ui-hooks/use-uncontrolled";
import { useUniqueId } from "@versini/ui-hooks/use-unique-id";

function AdvancedInput({ value, defaultValue, onChange, label, ...props }) {
  const [inputValue, setInputValue, isControlled] = useUncontrolled({
    value,
    defaultValue,
    finalValue: "",
    onChange
  });

  const inputId = useUniqueId("input");

  return (
    <div>
      <label htmlFor={inputId}>
        {label} {isControlled ? "(Controlled)" : "(Uncontrolled)"}
      </label>
      <input
        id={inputId}
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        {...props}
      />
    </div>
  );
}

License

MIT