CSS media queries handle most responsive styling, but sometimes your React components need to know about the viewport, user preferences, or device capabilities at the JavaScript level. Maybe you need to conditionally render a mobile navigation, detect dark mode, or respect reduced motion preferences. The useMediaQuery hook from ReactUse gives you a reactive boolean that stays in sync with any CSS media query string.
What is useMediaQuery?
useMediaQuery wraps the browser's window.matchMedia API in a React hook. Pass it a media query string and it returns a boolean. It subscribes to the change event internally, so the value updates automatically when conditions change.
import { useMediaQuery } from "@reactuses/core";
function Example() {
const isMobile = useMediaQuery("(max-width: 768px)");
return <p>{isMobile ? "Mobile view" : "Desktop view"}</p>;
}
The API is minimal:
useMediaQuery(query: string, defaultState?: boolean) => boolean
Basic Usage: Mobile Detection
The most common use case is detecting screen width for conditional rendering:
import { useMediaQuery } from "@reactuses/core";
function Navigation() {
const isMobile = useMediaQuery("(max-width: 767px)");
if (isMobile) {
return (
<button aria-label="Open menu">
<HamburgerIcon />
</button>
);
}
return (
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
);
}
The component only re-renders when the boolean flips, not on every resize pixel.
Reusable Breakpoints Pattern
Define your breakpoints once and reuse them across your app:
import { useMediaQuery } from "@reactuses/core";
function useBreakpoint() {
const isMobile = useMediaQuery("(max-width: 639px)");
const isTablet = useMediaQuery("(min-width: 640px) and (max-width: 1023px)");
const isDesktop = useMediaQuery("(min-width: 1024px)");
return { isMobile, isTablet, isDesktop };
}
function Dashboard() {
const { isMobile, isTablet } = useBreakpoint();
const columns = isMobile ? 1 : isTablet ? 2 : 4;
return (
<div style={{ display: "grid", gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: 16 }}>
<Card title="Revenue" />
<Card title="Users" />
<Card title="Orders" />
<Card title="Growth" />
</div>
);
}
Detecting User Preferences
Media queries go far beyond screen size. You can detect system-level preferences:
Dark Mode Detection
import { useMediaQuery } from "@reactuses/core";
function ThemeProvider({ children }: { children: React.ReactNode }) {
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
return (
<div style={{
background: prefersDark ? "#1a1a2e" : "#ffffff",
color: prefersDark ? "#e0e0e0" : "#1a1a1a",
minHeight: "100vh",
}}>
{children}
</div>
);
}
Reduced Motion
This is critical for accessibility. Users with vestibular disorders set this preference at the OS level:
import { useMediaQuery } from "@reactuses/core";
function AnimatedCard({ children }: { children: React.ReactNode }) {
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");
return (
<div style={{
transition: prefersReducedMotion ? "none" : "transform 0.3s ease",
}}>
{children}
</div>
);
}
Other Useful Queries
const prefersHighContrast = useMediaQuery("(prefers-contrast: high)");
const isPortrait = useMediaQuery("(orientation: portrait)");
const hasHover = useMediaQuery("(hover: hover)");
SSR and Hydration Safety
When rendering on the server, window.matchMedia does not exist. Without a defaultState, the hook returns false on the server and the real value on the client, causing a React hydration mismatch.
Pass a defaultState to prevent this:
const isMobile = useMediaQuery("(max-width: 768px)", false);
In development mode, the hook logs a console warning if you forget defaultState during SSR.
Combining with Other Hooks
useMediaQuery pairs naturally with other ReactUse hooks. Here is a theme switcher that respects system preference but lets the user override:
import { useMediaQuery, useLocalStorage } from "@reactuses/core";
function ThemeSwitcher() {
const systemPrefersDark = useMediaQuery("(prefers-color-scheme: dark)");
const [userTheme, setUserTheme] = useLocalStorage<"light" | "dark" | "system">("theme", "system");
const isDark = userTheme === "system" ? systemPrefersDark : userTheme === "dark";
return (
<div>
<p>Current theme: {isDark ? "dark" : "light"}</p>
<select value={userTheme} onChange={(e) => setUserTheme(e.target.value as "light" | "dark" | "system")}>
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
);
}
Common Mistakes to Avoid
- Using window.matchMedia directly in render -- you get a stale snapshot without subscribing to changes.
- Forgetting defaultState with SSR -- always provide one in Next.js, Remix, or Astro projects.
-
Too many individual queries -- group related breakpoints into a custom hook like
useBreakpoint.
Installation
npm i @reactuses/core
# or
pnpm add @reactuses/core
# or
yarn add @reactuses/core
FAQ
How is useMediaQuery different from CSS media queries?
CSS media queries apply styles declaratively. useMediaQuery gives you a JavaScript boolean so you can conditionally render components, run logic, or pass dynamic props. Use CSS for styling, use the hook when you need JavaScript-level awareness.
Does useMediaQuery cause performance issues?
No. Each call creates a single matchMedia listener, which is very lightweight. The component only re-renders when the boolean value actually changes, not on every resize event.
Can I use useMediaQuery with Next.js or other SSR frameworks?
Yes. Pass a defaultState parameter to avoid hydration mismatches. For example: useMediaQuery("(max-width: 768px)", false). The hook will use the default on the server and update to the real value on the client.
What media queries can I use besides screen width?
You can use any valid CSS media query: prefers-color-scheme, prefers-reduced-motion, prefers-contrast, orientation, hover, pointer, and more.
ReactUse provides 100+ production-ready hooks for React. Explore them all at reactuse.com →
Top comments (0)