Sensors
Viewport, network, device, and permission state — SSR-safe reads of the browser environment around your component.
Hooks that read ambient browser/device state — viewport size, color scheme, connectivity, device capabilities, permissions — and re-render when it changes. Every hook here has an SSR-safe default and updates on the client once it can subscribe to the underlying API.
useMediaQuery
Matches the current viewport against an arbitrary CSS media query string, re-rendering when the match changes. SSR-safe — returns false until the client subscribes via matchMedia.
const isWide = useMediaQuery("(min-width: 1024px)");
useIsMobile
Reports whether the viewport is below a breakpoint (768px by default) — (max-width: breakpoint - 1). Built on useMediaQuery, so it shares the same SSR-safe default.
const isMobile = useIsMobile(); // true below 768px
const isCompact = useIsMobile(1024); // true below 1024px
useIsClient
SSR-safe hydration guard — false during server rendering, true once mounted on the client. Useful for gating client-only rendering (portals, window-dependent UI) without a hydration mismatch.
const isClient = useIsClient();
return isClient ? <ClientOnlyWidget /> : null;
useIsServer
true during server rendering, false once mounted on the client — the inverse of useIsClient. Useful for skipping client-only work during SSR.
const isServer = useIsServer();
if (isServer) return null; // skip client-only work during SSR
useColorScheme
The OS/browser color scheme preference, via the prefers-color-scheme media feature. Defaults to "light" during server rendering and before the client subscribes to matchMedia.
const scheme = useColorScheme(); // "dark" | "light"
usePrefersDarkMode
true when the OS/browser prefers dark mode, via prefers-color-scheme: dark. false during server rendering and before the client subscribes to matchMedia.
const prefersDark = usePrefersDarkMode();
useOnlineStatus
Tracks navigator.onLine, updating on the online/offline window events. Defaults to true during server rendering and before the client subscribes — most visitors are online, so this avoids a false “offline” flash on the common path.
const isOnline = useOnlineStatus();
useNetworkState
navigator.onLine plus navigator.connection info (effectiveType, downlink, rtt, saveData) where the Network Information API is supported — undefined for those fields elsewhere. Updates on online/offline and connection change events. During server rendering, online defaults to true and the rest are undefined.
The Network Information API is Chromium-only — check its browser compatibility table — useOnlineStatus alone is the portable subset.
const { online, effectiveType, saveData } = useNetworkState();
usePreferredLanguage
navigator.language and navigator.languages, updating on the languagechange event. Falls back to "en" during server rendering and before the client subscribes.
const { language } = usePreferredLanguage(); // e.g. "en-US"
useOrientation
screen.orientation’s angle/type, updating on its change event and the legacy window orientationchange event. Falls back to { angle: 0 } during server rendering, before the client subscribes, and where the Screen Orientation API is unsupported.
const { angle, type } = useOrientation();
useGeolocation
Wraps the Geolocation API (navigator.geolocation). One-shot by default (getCurrentPosition); pass watch: true for continuous updates (watchPosition, cleaned up via clearWatch on unmount or option change). loading starts true and the effect — client-only — resolves it, so this is SSR-safe with no extra handling needed.
const { coords, loading, error } = useGeolocation();
// useGeolocation({ watch: true }) for continuous updates
useBattery
Wraps the Battery Status API (navigator.getBattery()) — Chromium-only, removed from most other browsers (browser compatibility). { supported: false } — the SSR-safe default — until the client confirms getBattery exists and resolves it.
const { supported, level, charging } = useBattery();
useWindowSize
window.innerWidth/innerHeight, updating on the resize event. Falls back to { width: 0, height: 0 } during server rendering and before the client subscribes.
const { width, height } = useWindowSize();
useDocumentVisibility
document.visibilityState, updating on the visibilitychange event. Falls back to "visible" during server rendering and before the client subscribes.
const visibility = useDocumentVisibility(); // "visible" | "hidden"
usePageLeave
Calls onPageLeave when the pointer leaves the viewport — a mouseout on document whose relatedTarget (and legacy toElement) are both null, meaning the pointer left the page entirely rather than moving between two elements inside it. Useful for exit-intent UI. onPageLeave doesn’t need to be memoized — the latest one is always called.
usePageLeave(() => setShowExitIntentModal(true));
useShare
Wraps the Web Share API (navigator.share()), with canShare support feature-detection. supported: false — the SSR-safe default — where navigator.share doesn’t exist.
const { share, canShare, supported } = useShare();
const data = { title: "Zap Studio", url: location.href };
if (supported && canShare(data)) await share(data);
usePermission
navigator.permissions.query({ name })’s state for the given permission ("granted" | "denied" | "prompt"), updating on the query result’s change event. undefined — the SSR-safe default — until the client resolves it, and permanently where the Permissions API is unsupported.
const cameraPermission = usePermission("camera");
useVibrate
Wraps navigator.vibrate() — mostly Android Chrome; no-op elsewhere (browser compatibility). supported: false — the SSR-safe default — where navigator.vibrate doesn’t exist, and vibrate() then always returns false.
const { vibrate, supported } = useVibrate();
if (supported) vibrate([100, 50, 100]);
useWakeLock
Screen Wake Lock API wrapper — not auto-acquired on mount; call request()/release() imperatively. Automatically released when the document is hidden (per spec the platform already does this, but this also proactively releases on visibilitychange) and on unmount. supported: false — the SSR-safe default — where navigator.wakeLock doesn’t exist.
const { active, request, release, supported } = useWakeLock();
if (supported) await request(); // keep the screen awake
useStorageEstimate
navigator.storage.estimate()’s usage/quota, one-shot on mount — no live updates, no refresh. supported reflects whether the Storage API is available; { supported: false } — the SSR-safe default — where it isn’t.
const { usage, quota, supported } = useStorageEstimate();
useDeviceCapabilities
navigator.hardwareConcurrency and navigator.deviceMemory (the latter Chromium-only — undefined elsewhere; see its browser compatibility). Static device capabilities — don’t change at runtime. { hardwareConcurrency: 0, deviceMemory: undefined } — the SSR-safe default — during server rendering.
const { hardwareConcurrency, deviceMemory } = useDeviceCapabilities();
useDeviceOrientation
Device tilt from the deviceorientation/deviceorientationabsolute events (accelerometer/magnetometer) — there’s no synchronous read, only the event, so this starts at { alpha: null, beta: null, gamma: null, absolute: false } (also the SSR-safe default) until one fires. iOS Safari gates this behind a user-gesture permission prompt — call requestPermission() from a click handler before relying on the values; it resolves true (no-op success) on platforms without the gate.
const { alpha, beta, gamma, supported, requestPermission } = useDeviceOrientation();
<button onClick={requestPermission}>Enable tilt controls</button>;
useDeviceMotion
Device acceleration from the devicemotion event — there’s no synchronous read, only the event, so this starts all-null (interval: 0, also the SSR-safe default) until one fires. Same iOS Safari user-gesture permission caveat as useDeviceOrientation — requestPermission() resolves true (no-op success) on platforms without the gate.
const { acceleration, supported, requestPermission } = useDeviceMotion();
<button onClick={requestPermission}>Enable motion controls</button>;
useVisualViewport
window.visualViewport’s geometry, updating on its resize/scroll events — this is what actually shrinks when an on-screen mobile keyboard opens, unlike useWindowSize’s innerHeight. Falls back to { width: 0, height: 0, offsetLeft: 0, offsetTop: 0, pageLeft: 0, pageTop: 0, scale: 1 } during server rendering, before the client subscribes, and where the Visual Viewport API is unsupported.
const { height, scale } = useVisualViewport();
useDevicePixelRatio
window.devicePixelRatio, updating on zoom or a monitor-move-driven DPR change. There’s no native “devicePixelRatio changed” event, so this subscribes to a (resolution: <current DPR>dppx) media query — which only ever matches once — and recreates it against the new DPR each time it fires. Useful for canvas/retina rendering. Falls back to 1 during server rendering and before the client subscribes.
const dpr = useDevicePixelRatio();
useTouchSupport
true when navigator.maxTouchPoints > 0. A static device capability — doesn’t change at runtime. false — the SSR-safe default — during server rendering.
const hasTouch = useTouchSupport();
useCookieEnabled
navigator.cookieEnabled. A static capability — doesn’t change at runtime. false — the SSR-safe default — during server rendering.
const cookiesEnabled = useCookieEnabled();
usePrintMode
true while the page is being printed (or previewed for print), via the print media query — more reliable than the raw beforeprint/afterprint events, which some browsers fire inconsistently around the print dialog. false during server rendering and before the client subscribes.
const isPrinting = usePrintMode();
useNotificationPermission
The Notifications API’s permission state, plus requestPermission() and a notify() helper that no-ops (returns undefined) unless permission is "granted". There’s no native “permission changed” event, so permission only updates when requestPermission() resolves. "unsupported" — the SSR-safe default — where the Notifications API doesn’t exist.
const { permission, requestPermission, notify } = useNotificationPermission();
await requestPermission();
notify("Done!", { body: "Your export finished." });
useFontsReady
true once document.fonts.ready resolves — custom web fonts have finished loading. Starts false (also the SSR-safe default); useful for delaying text render to avoid FOUC. Resolves immediately to true where the CSS Font Loading API is unsupported, rather than blocking forever.
const fontsReady = useFontsReady();
if (!fontsReady) return <Skeleton />;
useBrowserEngine
Identifies the browser’s rendering engine — "blink" (Chrome, Edge, Opera, and other Chromium browsers), "gecko" (Firefox), or "webkit" (Safari) — using feature detection instead of parsing navigator.userAgent, per MDN’s guidance against user-agent sniffing. "unknown" is also the safe default for server rendering.
This is a best-effort heuristic, meant for the rare cross-engine quirk that no single feature check covers — for example, only Safari requires a requestPermission() gesture before useDeviceOrientation/useDeviceMotion report values. Prefer detecting the specific feature or API you need over branching on the engine when you can.
const engine = useBrowserEngine();
const { requestPermission } = useDeviceOrientation();
return engine === "webkit" ? (
<button onClick={requestPermission}>Enable tilt controls</button>
) : null;
useExperimentalIdleDetector
Wraps the Idle Detection API’s IdleDetector — reports userState ("active"/"idle") and screenState ("locked"/"unlocked"), updating on its change event. start() requests the "idle-detection" permission first (resolving false if denied), then begins reporting; stop() tears the detector down. Both states stay undefined — the SSR-safe default — until start() resolves true.
Experimental per MDN, Chromium-only, requires a secure context — see MDN’s browser compatibility table.
const { userState, screenState, start, supported } = useExperimentalIdleDetector();
<button onClick={() => start({ threshold: 60_000 })} disabled={!supported}>
Watch for idle
</button>;
useExperimentalLocalFonts
Wraps window.queryLocalFonts() (Local Font Access API) — query() prompts for the "local-fonts" permission on first call and resolves the user’s locally installed fonts, or undefined when the user denies the prompt or the API is unsupported.
Experimental per MDN, Chromium-only — see MDN’s browser compatibility table.
const { query, supported } = useExperimentalLocalFonts();
const fonts = supported ? await query() : undefined;
useExperimentalNfc
Wraps the Web NFC API’s NDEFReader — scan() prompts for the "nfc" permission and starts listening for tags, resolving false when the API is missing or the user denies it; every tag that comes into range then updates reading with its serialNumber and raw records. stop() aborts the scan. write() and makeReadOnly() act on the next tag in range, each resolving false rather than throwing when the tag can’t be written. error holds the last failure — a rejected call, or a tag that couldn’t be decoded.
Records are handed over undecoded: data is a DataView, so text records are read with a TextDecoder built from the record’s own encoding.
Experimental per MDN, Chromium on Android only, and gated behind a secure context and a user gesture — see MDN’s browser compatibility table.
const { scan, reading, supported } = useExperimentalNfc();
<button onClick={() => scan()} disabled={!supported}>
Scan a tag
</button>;
{
reading ? <p>Tag {reading.serialNumber}</p> : null;
}
useExperimentalUserAgentData
navigator.userAgentData’s low-entropy fields (brands, mobile, platform) — a structured replacement for parsing navigator.userAgent. A static device capability — doesn’t change at runtime. undefined — the SSR-safe default — where User-Agent Client Hints is unsupported.
Experimental per MDN, Chromium-only — see MDN’s browser compatibility table.
const uaData = useExperimentalUserAgentData();
const isMobile = uaData?.mobile ?? false;
useExperimentalVirtualKeyboard
navigator.virtualKeyboard’s boundingRect, updating on its geometrychange event. Falls back to { x: 0, y: 0, width: 0, height: 0 } during server rendering, before the client subscribes, and where the API is unsupported.
Experimental per MDN, Chromium-only — see MDN’s browser compatibility table.
const { height } = useExperimentalVirtualKeyboard(); // on-screen keyboard height, in px
useExperimentalWindowManagement
Wraps the Window Management API — for placing windows across multiple screens. isExtended mirrors window.screen.isExtended (true once more than one display is connected), updating on the screen’s change event and needing no permission prompt. requestPermission() calls window.getScreenDetails(), resolving false when the API is missing or the user denies it; once granted, screens (every connected display) and currentScreen (the one showing this window) populate and stay live, updating on the underlying screenschange/currentscreenchange events. screens/currentScreen stay empty/undefined — the SSR-safe default — until permission is granted.
Experimental per MDN, Chromium-only, not Baseline — see MDN’s browser compatibility table.
const { isExtended, screens, currentScreen, requestPermission } = useExperimentalWindowManagement();
<button onClick={requestPermission} disabled={!isExtended}>
Show all {screens.length} screens
</button>;