@versini/ui-hooks
v6.1.1
Published
[](https://www.npmjs.com/package/@versini/ui-hooks) 
- 🔧 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-hooksNote: While this package contains React hooks without styling, when used alongside the UI component packages it assumes TailwindCSS and the
@versini/ui-stylesplugin 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 accessibilityuseIsMounted- Check if component is mounteduseMergeRefs- Merge multiple React refsuseUncontrolled- Manage controlled/uncontrolled state
Interaction Hooks
useClickOutside- Detect clicks outside an elementuseHotkeys- Handle keyboard shortcuts and hotkeysuseHaptic- Provide haptic feedback for mobile devices
Storage Hooks
useLocalStorage- Sync state with localStorage
Viewport and Size Hooks
useViewportSize- Track browser viewport dimensionsuseVisualViewportSize- Track visual viewport (mobile-friendly)useInViewport- Detect if element is visible in viewportuseResizeObserver- 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): stringParameters:
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 outsideevents: 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
): voidParameters:
hotkeys: Array of[shortcut, handler, options?]tuplestagsToIgnore: 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.vibratewhen 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 keyinitialValue: 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 intervalinterval: Interval time in milliseconds
Returns: Object with start/stop functions and active state
useIsMounted
Check if component is currently mounted.
const isMounted = useIsMounted(): () => booleanReturns: 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 valuedefaultValue: Initial uncontrolled valuefinalValue: Fallback value when others are undefinedonChange: Change handler for controlled modeinitialControlledDelay: 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
