Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useEffect, useCallback } from "react";
/**
* useMediaQuery
* Returns `true` when the given CSS media query matches.
* SSR-safe: returns `defaultValue` on the server.
*/
function useMediaQuery(
query: string,
defaultValue: boolean = false
): boolean {
const [matches, setMatches] = useState<boolean>(() => {
if (typeof window === "undefined") return defaultValue;
return window.matchMedia(query).matches;
});
useEffect(() => {
if (typeof window === "undefined") return;
const mql = window.matchMedia(query);
setMatches(mql.matches);
const handler = (e: MediaQueryListEvent) => {
setMatches(e.matches);
};
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
/**
* Convenience hooks built on useMediaQuery
*/
function useIsMobile(breakpoint: number = 768): boolean {
return useMediaQuery(`(max-width: ${breakpoint - 1}px)`);
}
function useIsDesktop(breakpoint: number = 1024): boolean {
return useMediaQuery(`(min-width: ${breakpoint}px)`);
}
function usePrefersDarkMode(): boolean {
return useMediaQuery("(prefers-color-scheme: dark)");
}
function usePrefersReducedMotion(): boolean {
return useMediaQuery("(prefers-reduced-motion: reduce)");
}When to reach for this: You need responsive behavior in JavaScript that CSS alone cannot handle, such as conditionally rendering components, loading different data, or adjusting hook parameters based on screen size.
"use client";
function ResponsiveNav() {
const isMobile = useIsMobile();
const prefersDark = usePrefersDarkMode();
const reducedMotion = usePrefersReducedMotion();
return (
<nav
style={{
background: prefersDark ? "#1a1a2e" : "#ffffff",
transition: reducedMotion ? "none" : "background 0.3s",
}}
>
{isMobile ? <HamburgerMenu /> : <DesktopMenu />}
</nav>
);
}
function HamburgerMenu() {
return <button aria-label="Menu">☰</button>;
}
function DesktopMenu() {
return (
<ul style={{ display: "flex", gap: 16, listStyle: "none" }}>
<li>Home</li>
<li>About</li>
<li>Contact</li>
</ul>
);
}
function AdaptiveGrid() {
const isDesktop = useIsDesktop();
const columns = isDesktop ? 3 : 1;
return (
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: 16,
}}
>
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
);
}What this demonstrates:
useIsMobile conditionally renders a hamburger menu vs. desktop navigationusePrefersDarkMode applies a theme without any CSS class togglingusePrefersReducedMotion disables CSS transitions for users who prefer reduced motionwindow.matchMedia creates a MediaQueryList object that evaluates a CSS media query string.change event fires whenever the match state flips (e.g., the window crosses a breakpoint), triggering a state update.defaultValue when window is not available. The effect only runs on the client.| Parameter | Type | Default | Description |
|---|---|---|---|
query | string | - | Any valid CSS media query string |
defaultValue | boolean | false | Returned during SSR or when matchMedia is unavailable |
| Returns | boolean | - | Whether the query currently matches |
Multiple queries: For complex responsive logic, call the hook multiple times:
const isSm = useMediaQuery("(min-width: 640px)");
const isMd = useMediaQuery("(min-width: 768px)");
const isLg = useMediaQuery("(min-width: 1024px)");Breakpoint object: Return a named breakpoint for Tailwind-style usage:
function useBreakpoint() {
const isSm = useMediaQuery("(min-width: 640px)");
const isMd = useMediaQuery("(min-width: 768px)");
const isLg = useMediaQuery("(min-width: 1024px)");
const isXl = useMediaQuery("(min-width: 1280px)");
if (isXl) return "xl";
if (isLg) return "lg";
if (isMd) return "md";
if (isSm) return "sm";
return "xs";
}boolean, so no generics are needed."xs" | "sm" | "md" | "lg" | "xl".defaultValue, but the client may evaluate to a different value. This can cause a flash. Fix: Accept the brief flash, or use CSS-based responsive design for layout-critical elements and reserve useMediaQuery for non-visual logic.useMediaQuery creates a separate matchMedia listener. Fix: This is fine for a handful of breakpoints. If you have dozens, consider a single listener with multiple breakpoints.matchMedia does not throw for invalid queries; it returns a MediaQueryList that never matches. Fix: Validate queries during development.addListener/removeListener instead of addEventListener. Fix: Modern Safari supports the standard API. For legacy support, add a fallback.| Package | Hook Name | Notes |
|---|---|---|
usehooks-ts | useMediaQuery | Similar API, well-tested |
@uidotdev/usehooks | useMediaQuery | Minimal implementation |
ahooks | useResponsive | Returns breakpoint object |
react-responsive | useMediaQuery | Supports server-side rendering with hints |
| Tailwind CSS | Responsive classes | CSS-only, no JS needed |
window.matchMedia(query) returns a MediaQueryList object.matches boolean and fires a change event when the match state flips.matches on mount and subscribes to change for live updates.They are thin wrappers that call useMediaQuery with a pre-built query string. For example, useIsMobile(768) calls useMediaQuery("(max-width: 767px)"). No extra logic is added.
max-width: 767px targets screens narrower than 768px. Using breakpoint - 1 ensures that exactly 768px wide is not considered mobile, matching common CSS breakpoint conventions.
defaultValue (false), but the client may evaluate differently.useMediaQuery for non-visual logic like data fetching or feature toggles.window.matchMedia does not throw for invalid queries. It returns a MediaQueryList that never matches. There is no browser-level validation. Verify your query strings during development.
Yes, each call creates its own matchMedia listener. For a handful of breakpoints this is fine. If you have dozens, consolidate into a single useBreakpoint hook that returns a named breakpoint string.
function useBreakpoint(): "xs" | "sm" | "md" | "lg" | "xl" {
const isSm = useMediaQuery("(min-width: 640px)");
const isMd = useMediaQuery("(min-width: 768px)");
const isLg = useMediaQuery("(min-width: 1024px)");
const isXl = useMediaQuery("(min-width: 1280px)");
if (isXl) return "xl";
if (isLg) return "lg";
if (isMd) return "md";
if (isSm) return "sm";
return "xs";
}During SSR, window.matchMedia is not available. defaultValue provides a safe fallback so the hook returns a predictable boolean on the server. It defaults to false.
Yes. Use the usePrefersReducedMotion convenience hook or pass the query directly:
const reducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");Reviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥