Hooks
useIdle
A user-idle detection hook that flips a boolean after a period of no pointer, keyboard, wheel, or touch activity.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/use-idle.jsonPrompt
Build a React + TypeScript "useIdle" hook (no dependencies beyond React;
browser event listeners + timers only).
Contract
- `useIdle(timeout?: number, options?: { events?: string[]; initialState?:
boolean }): boolean`.
- `timeout` defaults to 60000ms (60s).
- `options.events` defaults to `["pointermove", "pointerdown", "keydown",
"wheel", "touchstart"]`, attached to `window` with `{ passive: true }`;
passing a custom array replaces the default list entirely.
- `options.initialState` is the idle value used for the very first render
(default `false` — a fresh mount counts as "just active").
- Returns a single boolean: `true` once `timeout` ms have elapsed with none
of `events` firing, `false` otherwise.
Behavior
- On mount, schedule a timer for `timeout` ms that flips the return value to
`true` when it fires.
- Any of `events` firing resets the timer: clear the pending timeout, flip
the value back to `false` if it was `true`, and schedule a fresh `timeout`
ms timer.
- Throttle the reset path: `pointermove`/`wheel` can fire dozens of times a
second, and clearing + rescheduling a timer on every single one is wasted
work. Compare timestamps instead — only the first event after a ~1s
throttle window actually resets the timer; events inside that window are
dropped outright (not merely debounced). The idle flip can therefore land
up to ~1s later than the mathematically exact last-event instant, an
acceptable trade for cutting timer churn during continuous mouse/wheel
movement.
- `document`'s `visibilitychange` is wired in separately from `events` and
cannot be turned off through `options.events`: switching back to the tab
(`visibilityState === "visible"`) counts as one activity event and resets
the timer through the same throttled path. Switching away (`hidden`) is
deliberately a no-op — it does NOT force an early idle flip. The already
scheduled timer keeps counting down and fires naturally when it elapses
(background tabs may get their timers throttled by the browser, and that
drift is accepted rather than special-cased).
- `timeout` or `options.events` changing after mount tears down the old
timer/listeners and rebuilds them, without losing the currently displayed
idle/active value.
- Every state update happens inside an event listener or timer callback —
never synchronously during render or in a bare mount effect — so the hook
is SSR-safe.
- On unmount, clear the pending timeout and remove every listener
(`events` + `visibilitychange`); nothing fires after the consumer is gone.
Rendering & styling
- The hook renders nothing itself and owns no visual state. Consumers own
all UI: swap an icon/label/opacity when the returned boolean flips, using
semantic tokens (`text-muted-foreground`, `border`, `grayscale`) for any
idle styling, and respect `prefers-reduced-motion` for any transition.
Customization levers
- `timeout` — how long without activity before flipping to idle; shorter
for "hide the cursor", longer for "warn before logout".
- `options.events` — narrow the activity signal (e.g. `["keydown"]` only) or
widen it (add `"scroll"`, `"click"`) depending on what should count as
"the user is here".
- `options.initialState` — start already idle (e.g. a kiosk screen that
should show its idle state immediately) instead of the default active.
- Cross-tab idle sync (all open tabs agree the user stepped away) is
intentionally NOT built in — it would need a shared heartbeat through
`localStorage`/`BroadcastChannel`, a meaningfully bigger and more
failure-prone feature than this hook's single-tab timer.
- The browser's native Idle Detection API (`IdleDetector`) is also
intentionally NOT used by default: it requires an explicit permission
prompt and is Chromium-only, a much heavier ask than a plain event
listener for the vast majority of "dim the UI after inactivity" cases.Concepts
- Throttled reset, not debounced clear — high-frequency events (
pointermove,wheel) don't each clear + reschedule the timer; a timestamp comparison drops everything inside a 1s window and only the first event past it actually resets, trading a little precision for a lot less timer churn. - Visibility as a one-way activity signal — coming back to the tab counts as activity and resets the timer, but leaving the tab never forces an idle flip; the pending timeout is left to expire on its own, so idle detection stays driven by one mechanism instead of two competing ones.
- Boolean-only contract — the hook exposes no imperative
reset()/pause(); every actor (mouse, keyboard, tab visibility) resets state the same way, through the same event listeners, keeping the state machine to two nodes instead of a bespoke API surface. - Not a security boundary — idle here is purely a client-observed signal; it disappears on a page refresh or a spoofed system clock, so it should drive UX (warnings, dimming, paused polling) and never gate anything a server should be the source of truth for.
useFullscreen
A hook that puts a single element into and out of the browser's native fullscreen mode, keeping a `fullscreen` boolean in sync — including when the user presses Esc.
useAsync
A one-call async state machine — idle/pending/success/error with last-call-wins race protection, AbortSignal cancellation and unmount-safe resolves.