@sharpbits/react-interaction-hooks
v0.3.0
Published
A collection of 63 React hooks for UI interactions — pointer, touch, keyboard, focus, scroll, resize, clipboard, device sensors, and more. Zero dependencies, tree-shakeable, lightweight, and blazing fast.
Downloads
1,519
Maintainers
Keywords
Readme
@sharpbits/react-interaction-hooks
A collection of 63 React hooks for UI interactions — pointer, touch, keyboard, focus, scroll, resize, clipboard, device sensors, timing, and more. Zero dependencies, tree-shakeable, lightweight, and blazing fast.
Installation
npm install @sharpbits/react-interaction-hooksHooks
Pointer & Mouse
| Hook | Description |
|------|-------------|
| useOutsideClick | Detect clicks/touches outside an element |
| useHover | Track hover state with optional delay |
| useDrag | Track drag position and delta |
| usePointerPosition | Track mouse position on the page |
| useContextMenu | Intercept right-click on an element |
| useMouseLeaveWindow | Detect when mouse exits the browser window |
| useDoubleClick | Detect double-click on an element (mouse) |
| usePointerLock | Lock/unlock pointer for raw mouse movement |
Touch
| Hook | Description |
|------|-------------|
| useSwipe | Detect swipe direction from touch |
| useDoubleTap | Detect double-tap on touch devices |
| usePinch | Detect pinch-to-zoom gesture |
| useLongPress | Detect long press on mouse and touch |
Keyboard
| Hook | Description |
|------|-------------|
| useKeyPress | Track whether a specific key is held down |
| useKeyCombo | Listen for keyboard shortcuts |
| useArrowNavigation | Navigate a list with arrow keys |
| useFocusTrap | Trap Tab focus inside an element |
| useKeySequence | Detect ordered key sequences (Konami code style) |
Focus & Visibility
| Hook | Description |
|------|-------------|
| useFocusWithin | Detect focus inside an element |
| useIntersectionObserver | Detect when an element enters/leaves the viewport |
| useIdle | Detect user inactivity |
| usePageVisibility | Detect when the browser tab is hidden |
| useFocusReturn | Restore focus to the trigger element on unmount |
| useTabFocus | Detect whether user is navigating via keyboard |
Scroll
| Hook | Description |
|------|-------------|
| useScrollPosition | Track scroll X/Y position |
| useScrollDirection | Detect scroll direction (up/down) |
| useScrollProgress | Scroll progress as a 0–1 value |
| useScrollLock | Lock body scroll (iOS Safari safe) |
| useScrollIntoView | Scroll an element into view programmatically |
| useScrollSpy | Track which section is active based on scroll |
| useInfiniteScroll | Trigger a callback when a sentinel element is visible |
Resize & DOM
| Hook | Description |
|------|-------------|
| useResizeObserver | Track element width/height changes |
| useWindowSize | Track browser window dimensions |
| useElementSize | Track rendered size of an element via ResizeObserver |
| useElementPosition | Track viewport-relative position of an element |
| useContainerQuery | Match element width against named breakpoints |
| useMutationObserver | Observe DOM mutations on an element |
| useFullscreen | Enter/exit fullscreen on an element |
Clipboard & Media
| Hook | Description |
|------|-------------|
| useCopyToClipboard | Copy text to clipboard |
| useMediaQuery | Subscribe to a CSS media query |
| useTextSelection | Track selected text and its bounding rect |
| useDropZone | Accept file drag-and-drop on an element |
| usePaste | Listen for paste events anywhere on the page |
| useShare | Trigger the native Web Share dialog |
| usePermission | Query browser permission state |
| useNotification | Request permission and send desktop notifications |
| useReducedMotion | Detect prefers-reduced-motion preference |
| useColorScheme | Detect prefers-color-scheme (dark/light) |
Device & Sensors
| Hook | Description |
|------|-------------|
| useDeviceOrientation | Read gyroscope orientation (alpha/beta/gamma) |
| useGeolocation | Watch GPS coordinates |
| useNetworkStatus | Track online/offline and connection type |
| useBattery | Read battery level and charging state |
| useVibrate | Trigger device vibration |
| useWakeLock | Prevent screen from sleeping |
| useGamepad | Detect connected gamepads |
| useSpeechRecognition | Convert speech to text via Web Speech API |
| useSpeechSynthesis | Convert text to speech |
| useEyeDropper | Pick a color from the screen |
Timing
| Hook | Description |
|------|-------------|
| useDebounce | Debounce a value by a given delay |
| useThrottle | Throttle a value to update at most once per interval |
| useInterval | Run a callback on a repeating interval |
| useTimeout | Run a callback after a delay, with reset/clear |
| useAnimationFrame | Run a callback on every animation frame |
| useCountdown | Countdown timer with start, stop, and reset |
Reference
useOutsideClick
Fires a callback when the user clicks or touches outside a given element.
const ref = useRef<HTMLDivElement>(null)
useOutsideClick(ref, () => setOpen(false))useOutsideClick(ref, handler, enabled?)| Param | Type | Default |
|-------|------|---------|
| ref | RefObject<HTMLElement> | — |
| handler | (event: MouseEvent \| TouchEvent) => void | — |
| enabled | boolean | true |
useHover
Returns true while the mouse is over the element. Supports enter/leave delays to prevent flickering.
const ref = useRef<HTMLDivElement>(null)
const isHovered = useHover(ref, { enterDelay: 200 })useHover(ref, options?) → boolean| Option | Type | Default |
|--------|------|---------|
| enterDelay | number (ms) | 0 |
| leaveDelay | number (ms) | 0 |
useDrag
Tracks mouse drag on an element, returning whether a drag is active, the total delta, and the current pointer position.
const ref = useRef<HTMLDivElement>(null)
const { isDragging, delta, position } = useDrag(ref)useDrag(ref) → { isDragging: boolean, delta: {x,y}, position: {x,y} }usePointerPosition
Tracks the mouse pointer position across the entire page.
const { x, y, clientX, clientY } = usePointerPosition()usePointerPosition() → { x, y, clientX, clientY }x/y are page-relative (include scroll); clientX/clientY are viewport-relative.
useContextMenu
Intercepts right-click on an element, prevents the default browser menu, and calls the handler.
const ref = useRef<HTMLDivElement>(null)
useContextMenu(ref, (e) => openMenu(e.clientX, e.clientY))useContextMenu(ref, callback)useMouseLeaveWindow
Fires when the mouse pointer exits the browser window entirely. Useful for exit-intent popups.
useMouseLeaveWindow(() => setShowExitPopup(true))useMouseLeaveWindow(callback)useSwipe
Detects swipe direction from touch events on an element.
const ref = useRef<HTMLDivElement>(null)
useSwipe(ref, {
onSwipe: (direction) => console.log(direction) // "left" | "right" | "up" | "down"
})useSwipe(ref, { onSwipe, threshold? })| Option | Type | Default |
|--------|------|---------|
| onSwipe | (dir: "left"\|"right"\|"up"\|"down") => void | — |
| threshold | number (px) | 50 |
useDoubleTap
Fires a callback when the user double-taps an element (touch only).
const ref = useRef<HTMLDivElement>(null)
useDoubleTap(ref, () => like())useDoubleTap(ref, callback, options?)| Option | Type | Default |
|--------|------|---------|
| threshold | number (ms between taps) | 300 |
usePinch
Tracks a two-finger pinch gesture, reporting scale and midpoint origin.
const ref = useRef<HTMLDivElement>(null)
usePinch(ref, {
onPinch: ({ scale, origin }) => setScale(scale),
onPinchEnd: ({ scale }) => commitScale(scale),
})usePinch(ref, { onPinch, onPinchEnd? })scale is relative to the start of the gesture (1.0 = no change).
useLongPress
Fires a callback after the user holds down on an element for the specified duration.
const ref = useRef<HTMLButtonElement>(null)
useLongPress(ref, () => deleteItem(), {
threshold: 800,
onStart: () => showProgressBar(),
onCancel: () => hideProgressBar(),
})useLongPress(ref, callback, options?)| Option | Type | Default |
|--------|------|---------|
| threshold | number (ms) | 500 |
| onStart | () => void | — |
| onCancel | () => void | — |
onCancelis not called after a successful long press.
useKeyPress
Returns true while a specific key is held down.
const isShiftHeld = useKeyPress("Shift")
const isEscape = useKeyPress("Escape")useKeyPress(key: string) → booleanKey names follow the KeyboardEvent.key spec.
useKeyCombo
Listens for a keyboard shortcut and fires a callback.
useKeyCombo("ctrl+k", (e) => { e.preventDefault(); openSearch() })
useKeyCombo("ctrl+shift+p", openCommandPalette)
useKeyCombo("escape", closeModal)useKeyCombo(combo, callback, options?)Combo syntax: "k" · "escape" · "ctrl+k" · "ctrl+shift+k" · "meta+k"
| Option | Type | Default |
|--------|------|---------|
| enabled | boolean | true |
| target | HTMLElement \| Document | document |
useArrowNavigation
Handles arrow key navigation within a container, moving focus between focusable children.
const ref = useRef<HTMLUListElement>(null)
useArrowNavigation(ref, { orientation: "vertical", loop: true })
return (
<ul ref={ref}>
<li tabIndex={0}>Item 1</li>
<li tabIndex={0}>Item 2</li>
</ul>
)useArrowNavigation(ref, options?)| Option | Type | Default |
|--------|------|---------|
| selector | string | "[role='option'], [role='menuitem'], li, button" |
| orientation | "vertical" \| "horizontal" \| "both" | "vertical" |
| loop | boolean | true |
useFocusTrap
Traps Tab/Shift+Tab focus inside an element and focuses the first focusable element on activation. Essential for accessible modals and dialogs.
const ref = useRef<HTMLDivElement>(null)
useFocusTrap(ref, isModalOpen)
return <div ref={ref}>
<input />
<button>Close</button>
</div>useFocusTrap(ref, enabled?)useFocusWithin
Returns true when focus is inside an element or any of its descendants.
const ref = useRef<HTMLFormElement>(null)
const active = useFocusWithin(ref)useFocusWithin(ref) → booleanuseIntersectionObserver
Observes when an element enters or leaves the viewport. Useful for lazy loading and infinite scroll.
const ref = useRef<HTMLDivElement>(null)
const { isIntersecting, ratio } = useIntersectionObserver(ref, {
threshold: 0.5,
})useIntersectionObserver(ref, options?) → { isIntersecting: boolean, ratio: number }| Option | Type | Default |
|--------|------|---------|
| threshold | number \| number[] | 0 |
| rootMargin | string | "0px" |
| root | Element \| null | null |
useIdle
Returns true after the user has had no mouse/keyboard/touch/scroll activity for the given duration.
const isIdle = useIdle(30_000) // 30 seconds
if (isIdle) lockScreen()useIdle(timeout: number) → booleanusePageVisibility
Returns true when the current browser tab is visible, false when hidden (minimised or switched away).
const isVisible = usePageVisibility()
useEffect(() => { if (!isVisible) pauseVideo() }, [isVisible])usePageVisibility() → booleanuseScrollPosition
Tracks the current scroll position of the window.
const { x, y } = useScrollPosition()useScrollPosition() → { x: number, y: number }useScrollDirection
Returns the current scroll direction or null before the first scroll.
const direction = useScrollDirection() // "up" | "down" | nulluseScrollDirection() → "up" | "down" | nulluseScrollProgress
Returns scroll progress as a value from 0 to 1. Pass a ref to track an element's internal scroll, or omit it to track the page.
const progress = useScrollProgress() // page scroll
const progress = useScrollProgress(ref) // element scrolluseScrollProgress(ref?) → numberuseScrollLock
Locks body scroll when locked is true. Uses position: fixed to prevent bounce on iOS Safari and restores the scroll position on unlock.
useScrollLock(isModalOpen)useScrollLock(locked: boolean): voiduseResizeObserver
Tracks the content dimensions of an element using ResizeObserver.
const ref = useRef<HTMLDivElement>(null)
const { width, height } = useResizeObserver(ref)useResizeObserver(ref) → { width: number, height: number }useWindowSize
Tracks the browser window's inner dimensions.
const { width, height } = useWindowSize()useWindowSize() → { width: number, height: number }useCopyToClipboard
Copies text to the clipboard and provides a copied flag that resets after a delay.
const { copied, copy } = useCopyToClipboard()
return (
<button onClick={() => copy("Hello!")}>
{copied ? "Copied!" : "Copy"}
</button>
)useCopyToClipboard(resetDelay?) → { copied: boolean, copy: (text: string) => Promise<void> }| Param | Type | Default |
|-------|------|---------|
| resetDelay | number (ms) | 2000 |
useMediaQuery
Subscribes to a CSS media query and returns whether it currently matches.
const isDark = useMediaQuery("(prefers-color-scheme: dark)")
const isMobile = useMediaQuery("(max-width: 768px)")useMediaQuery(query: string) → booleanuseTextSelection
Tracks the user's current text selection anywhere on the page.
const { text, rect } = useTextSelection()
// rect is the DOMRect of the selection — useful for positioning a tooltipuseTextSelection() → { text: string, rect: DOMRect | null }useDropZone
Makes an element a file drop target. Returns isOver to indicate an active drag-over state.
const ref = useRef<HTMLDivElement>(null)
const { isOver } = useDropZone(ref, {
onDrop: (files) => uploadFiles(files),
})useDropZone(ref, { onDrop, onDragOver?, onDragLeave? }) → { isOver: boolean }useDeviceOrientation
Reads the device orientation from the gyroscope. Returns null values on desktop.
const { alpha, beta, gamma } = useDeviceOrientation()useDeviceOrientation() → { alpha: number | null, beta: number | null, gamma: number | null }useGeolocation
Watches GPS coordinates via the Geolocation API.
const { loading, position, error } = useGeolocation({ enableHighAccuracy: true })
const lat = position?.coords.latitudeuseGeolocation(options?) → { loading, position: GeolocationPosition | null, error: GeolocationPositionError | null }| Option | Type | Default |
|--------|------|---------|
| enableHighAccuracy | boolean | false |
| timeout | number (ms) | Infinity |
| maximumAge | number (ms) | 0 |
useNetworkStatus
Tracks online/offline status and connection type via the Network Information API.
const { online, effectiveType } = useNetworkStatus()
// effectiveType: "slow-2g" | "2g" | "3g" | "4g" | nulluseNetworkStatus() → { online: boolean, type: string | null, effectiveType: string | null }useBattery
Reads battery level and charging state via the Battery Status API (Chrome only).
const { supported, loading, level, charging, dischargingTime } = useBattery()useBattery() → { supported, loading, level: number | null, charging: boolean | null, chargingTime: number | null, dischargingTime: number | null }useVibrate
Returns a function that triggers device vibration. No-ops silently on unsupported devices.
const vibrate = useVibrate()
vibrate(200) // 200ms buzz
vibrate([100, 50, 100]) // pattern: buzz, pause, buzz
vibrate(0) // stopuseVibrate() → (pattern: number | number[]) => voiduseDoubleClick
Fires a callback when the user double-clicks an element (mouse only — for touch use useDoubleTap).
const ref = useRef<HTMLDivElement>(null)
useDoubleClick(ref, () => likePost())useDoubleClick(ref, callback, options?)| Option | Type | Default |
|--------|------|---------|
| threshold | number (ms between clicks) | 300 |
usePointerLock
Locks the pointer to an element, hiding the cursor and providing raw mouse delta. Used in games and 3D editors.
const ref = useRef<HTMLDivElement>(null)
const { isLocked, lock, unlock } = usePointerLock(ref)usePointerLock(ref) → { isLocked: boolean, lock: () => Promise<void>, unlock: () => void }useKeySequence
Fires a callback when the user presses a specific sequence of keys in order.
useKeySequence(
["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown"],
() => activateCheats()
)useKeySequence(sequence: string[], callback: () => void)useFocusReturn
Captures the currently focused element on mount and restores focus to it when the component unmounts. Essential for accessible modals.
function Modal() {
useFocusReturn()
return <dialog>...</dialog>
}useFocusReturn() → voiduseTabFocus
Returns true when the user is navigating via the Tab key, false when using mouse/touch. Useful for showing focus rings only for keyboard users.
const isTabFocused = useTabFocus()useTabFocus() → booleanuseScrollIntoView
Returns a function that smoothly scrolls a ref element into view.
const ref = useRef<HTMLDivElement>(null)
const scrollTo = useScrollIntoView(ref, { behavior: "smooth", block: "center" })
<button onClick={scrollTo}>Go to section</button>useScrollIntoView(ref, options?) → () => voiduseScrollSpy
Watches multiple section refs and returns the index of the one currently most visible in the viewport.
const active = useScrollSpy([ref1, ref2, ref3], { threshold: 0.5 })useScrollSpy(refs, options?) → number| Option | Type | Default |
|--------|------|---------|
| threshold | number | 0.5 |
| rootMargin | string | "0px" |
useInfiniteScroll
Fires an async callback when a sentinel element enters the viewport. Use a ref on a bottom marker to load more items.
const { loading } = useInfiniteScroll(sentinelRef, async () => {
const more = await fetchNextPage()
setItems(prev => [...prev, ...more])
})useInfiniteScroll(ref, onLoadMore, options?) → { loading: boolean }useElementSize
Tracks the rendered width and height of an element using ResizeObserver + getBoundingClientRect.
const ref = useRef<HTMLDivElement>(null)
const { width, height } = useElementSize(ref)useElementSize(ref) → { width: number, height: number }useElementPosition
Tracks the viewport-relative bounding rect of an element, updating on scroll and resize.
const ref = useRef<HTMLDivElement>(null)
const { top, left, width, height } = useElementPosition(ref)useElementPosition(ref) → { x, y, top, left, right, bottom, width, height }useContainerQuery
Matches an element's width against named breakpoints using ResizeObserver.
const ref = useRef<HTMLDivElement>(null)
const matches = useContainerQuery(ref, { sm: 300, md: 600, lg: 900 })
// matches.md === true when element width >= 600pxuseContainerQuery(ref, breakpoints: Record<string, number>) → Record<string, boolean>useMutationObserver
Observes DOM mutations on an element and fires a callback on each change.
const ref = useRef<HTMLDivElement>(null)
useMutationObserver(ref, (mutations) => console.log(mutations))useMutationObserver(ref, callback, options?)Default options: { childList: true, subtree: true }
useFullscreen
Controls the Fullscreen API for a given element.
const ref = useRef<HTMLDivElement>(null)
const { isFullscreen, enter, exit, toggle } = useFullscreen(ref)useFullscreen(ref) → { isFullscreen: boolean, enter: () => Promise<void>, exit: () => Promise<void>, toggle: () => Promise<void> }usePaste
Listens for paste events anywhere on the page and calls the callback with the pasted text.
usePaste((text, event) => {
console.log("Pasted:", text)
})usePaste(callback: (text: string, event: ClipboardEvent) => void)useShare
Wraps the Web Share API. Falls back gracefully on unsupported browsers.
const { supported, share } = useShare()
await share({ title: "Hello", url: window.location.href })useShare() → { supported: boolean, share: (data: ShareData) => Promise<void> }usePermission
Queries the Permissions API for the current state of a browser permission.
const state = usePermission("camera") // "granted" | "denied" | "prompt" | nullusePermission(name: PermissionName) → "granted" | "denied" | "prompt" | nulluseNotification
Requests notification permission and provides a notify helper.
const { permission, requestPermission, notify } = useNotification()
await requestPermission()
notify("Hello!", { body: "World" })useNotification() → { supported, permission, requestPermission, notify }useReducedMotion
Returns true when the user has requested reduced motion via OS/browser settings.
const reduced = useReducedMotion()
const duration = reduced ? 0 : 300useReducedMotion() → booleanuseColorScheme
Returns the user's preferred color scheme.
const scheme = useColorScheme() // "dark" | "light"useColorScheme() → "dark" | "light"useWakeLock
Prevents the screen from sleeping using the Screen Wake Lock API (Chrome/Edge only).
const { supported, active, request, release } = useWakeLock()useWakeLock() → { supported: boolean, active: boolean, request: () => Promise<void>, release: () => Promise<void> }useGamepad
Detects connected gamepads via the Gamepad API.
const { gamepads, connected } = useGamepad()useGamepad() → { gamepads: Gamepad[], connected: boolean }useSpeechRecognition
Converts speech to text using the Web Speech API (Chrome/Edge only).
const { supported, listening, transcript, start, stop, reset } = useSpeechRecognition()useSpeechRecognition(lang?) → { supported, listening, transcript, start, stop, reset }| Param | Type | Default |
|-------|------|---------|
| lang | string | "en-US" |
useSpeechSynthesis
Converts text to speech using the SpeechSynthesis API.
const { speak, speaking, voices, cancel } = useSpeechSynthesis()
speak("Hello world", { rate: 1.2, pitch: 1 })useSpeechSynthesis() → { supported, speaking, voices, speak, cancel }useEyeDropper
Opens the browser's color picker to sample a color from anywhere on the screen (Chrome 95+).
const { supported, color, open } = useEyeDropper()
const hex = await open() // e.g. "#ff5733"useEyeDropper() → { supported: boolean, color: string | null, open: () => Promise<string | null> }useDebounce
Returns a debounced copy of a value that only updates after the specified delay.
const [search, setSearch] = useState("")
const debounced = useDebounce(search, 400)
useEffect(() => {
fetchResults(debounced)
}, [debounced])useDebounce<T>(value: T, delay: number) → TuseThrottle
Returns a throttled copy of a value that updates at most once per interval.
const [pos, setPos] = useState({ x: 0, y: 0 })
const throttled = useThrottle(pos, 100)useThrottle<T>(value: T, delay: number) → TuseInterval
Runs a callback on a repeating interval. Pass null as the delay to pause.
useInterval(() => setCount(c => c + 1), running ? 1000 : null)useInterval(callback: () => void, delay: number | null)useTimeout
Fires a callback after a delay. Returns reset and clear controls.
const { reset, clear } = useTimeout(() => setFired(true), 3000)useTimeout(callback, delay) → { reset: () => void, clear: () => void }useAnimationFrame
Calls a callback on every animation frame, passing the delta time since the last frame.
useAnimationFrame((deltaTime) => {
setAngle(a => (a + deltaTime * 0.1) % 360)
})useAnimationFrame(callback: (deltaTime: number) => void)useCountdown
A countdown timer with start, stop, and reset controls.
const { count, running, start, stop, reset } = useCountdown(60)useCountdown(initialSeconds: number) → { count, running, start, stop, reset }Requirements
- React 16.8+
License
MIT
