Skip to main content

UI Utility Hooks

A collection of hooks for common UI concerns: responsive detection, scroll behavior, sticky elements, click-outside detection, portals, and ref composition.

useIsMobile

Detects whether the viewport is below a given breakpoint using window.matchMedia.

useIsMobile(breakpoint?: number): boolean
ParameterTypeDefaultDescription
breakpointnumber768Pixel width threshold for mobile detection

Returns: boolean -- true when window.innerWidth < breakpoint.

import { useIsMobile } from '@/hooks/use-mobile';

function ResponsiveLayout() {
const isMobile = useIsMobile();
// Custom breakpoint
const isTablet = useIsMobile(1024);

return isMobile ? <MobileNav /> : <DesktopNav />;
}

useScrollToTop

Provides smooth-scroll-to-top functionality with configurable easing, plus a helper that scrolls first then navigates to a new route.

useScrollToTop(options?: UseScrollToTopOptions): {
scrollToTop: (customDuration?: number) => void;
navigateWithScroll: (path: string, scrollDuration?: number) => void;
isScrolled: () => boolean;
smoothScrollTo: (targetY: number, customDuration?: number) => void;
}

Options

OptionTypeDefaultDescription
behaviorScrollBehavior"smooth"Native scroll behavior fallback
delaynumber150Base delay (ms) before navigation after scroll
offsetnumber0Target scroll Y position
thresholdnumber50Pixel threshold for isScrolled()
durationnumber800Animation duration in ms
easingstring"easeInOut"One of linear, easeInOut, easeOut, easeIn, bounceOut

Return Values

PropertyTypeDescription
scrollToTop(customDuration?) => voidScroll to offset with animation
navigateWithScroll(path, scrollDuration?) => voidScroll to top then route to path
isScrolled() => booleanReturns true if past threshold
smoothScrollTo(targetY, customDuration?) => voidScroll to arbitrary Y position
import { useScrollToTop } from '@/hooks/use-scroll-to-top';

function BackToTop() {
const { scrollToTop, isScrolled } = useScrollToTop({
duration: 600,
easing: 'bounceOut',
});

if (!isScrolled()) return null;
return <button onClick={() => scrollToTop()}>Back to Top</button>;
}

useStickyState

Tracks whether an element has become sticky using IntersectionObserver on a sentinel element placed above the sticky target.

useStickyState(options?: UseStickyStateOptions): {
isSticky: boolean;
sentinelRef: React.RefObject<HTMLDivElement | null>;
targetRef: React.RefObject<HTMLDivElement | null>;
}
OptionTypeDefaultDescription
thresholdnumber0IntersectionObserver threshold (0-1)
rootMarginstring"0px"IntersectionObserver root margin
debugbooleanfalseLog state changes to console
const { isSticky, sentinelRef, targetRef } = useStickyState();

return (
<>
<div ref={sentinelRef} className="h-4 w-full" />
<div
ref={targetRef}
className={`sticky top-0 ${isSticky ? 'shadow-lg' : ''}`}
>
Header content
</div>
</>
);

useStickyHeader

A simpler scroll-position-based sticky detector with a fixed 250px threshold.

useStickyHeader({ enableSticky }: { enableSticky?: boolean }): { isSticky: boolean }

useSkeletonVisibility

Controls skeleton loading display so that skeletons only appear on initial page loads, not during client-side navigation.

useSkeletonVisibility(isLoading: boolean, hasData?: boolean): boolean
ParameterTypeDefaultDescription
isLoadingboolean--Current loading state from data fetching
hasDatabooleanfalseWhether data already exists

Returns: boolean -- true only when it is the initial page load AND data is loading AND no data exists yet.

const showSkeleton = useSkeletonVisibility(isLoading, items.length > 0);

return showSkeleton ? <ItemSkeleton /> : <ItemList items={items} />;

useThrottledScroll

Attaches a scroll listener throttled via requestAnimationFrame for optimal 60fps performance.

useThrottledScroll(callback: () => void, enabled?: boolean): void
ParameterTypeDefaultDescription
callback() => void--Function called on each animation frame during scroll
enabledbooleantrueToggle the scroll listener on/off
const handleScroll = useCallback(() => {
setScrollY(window.scrollY);
}, []);

useThrottledScroll(handleScroll, isActive);

useOnClickOutside

Detects clicks (mouse and touch) outside a referenced element and invokes a handler.

useOnClickOutside<T extends HTMLElement>(
handler: (event: MouseEvent | TouchEvent) => void
): RefObject<T | null>
ParameterTypeDescription
handler(event) => voidCalled when a click occurs outside the referenced element

Returns: RefObject<T | null> -- Attach this ref to the element you want to monitor.

const dropdownRef = useOnClickOutside<HTMLDivElement>(() => {
setIsOpen(false);
});

return <div ref={dropdownRef}>{isOpen && <DropdownMenu />}</div>;

usePortal

Creates and manages a portal container DOM element for rendering content outside the normal DOM hierarchy.

usePortal(id?: string): HTMLDivElement | null
ParameterTypeDefaultDescription
idstring"portal-root"ID for the portal root element in the document body

Returns: HTMLDivElement | null -- The portal container, or null before mount.

import { createPortal } from 'react-dom';

const portalTarget = usePortal('modal-portal');

return (
<>
<button onClick={() => setOpen(true)}>Open</button>
{portalTarget && open &&
createPortal(<Modal onClose={() => setOpen(false)} />, portalTarget)
}
</>
);

useComposedRef

Composes a library-owned ref with a user-supplied ref (callback or object) into a single callback ref. Useful in component libraries that need to forward refs while keeping an internal reference.

useComposedRef<T extends HTMLElement>(
libRef: React.RefObject<T | null>,
userRef: UserRef<T>
): (instance: T | null) => void
ParameterTypeDescription
libRefRefObject<T | null>The library's internal ref object
userRefUserRef<T>A callback ref, ref object, null, or undefined from the consumer

Returns: A callback ref that updates both refs when the DOM node mounts/unmounts.

const internalRef = useRef<HTMLDivElement>(null);
const composedRef = useComposedRef(internalRef, forwardedRef);

return <div ref={composedRef}>...</div>;

Summary Table

HookPurposeSource File
useIsMobileResponsive breakpoint detectionuse-mobile.ts
useScrollToTopSmooth scroll with easing + navigateuse-scroll-to-top.ts
useStickyStateIntersectionObserver sticky detectionuse-sticky-state.ts
useStickyHeaderSimple scroll-based sticky detectionuse-sticky-state.ts
useSkeletonVisibilityInitial-load-only skeleton displayuse-skeleton-visibility.ts
useThrottledScrollRAF-throttled scroll listeneruse-throttled-scroll.ts
useOnClickOutsideClick-outside detectionuse-on-click-outside.ts
usePortalPortal container managementuse-portal.ts
useComposedRefCompose multiple refsuse-composed-ref.ts