
CSS Media Queries are fantastic for styling changing font sizes, padding, or grid layouts based on screen width.
But what happens when you need to change the logic or the structure of your application?
For example, on a Desktop, you might want a persistent Sidebar on the left. But on Mobile, that same sidebar needs to be a Sheet or a Modal that slides in when a button is clicked. You can't easily achieve this with just CSS display: none because the components function differently.
This is where we need Javascript Media Queries.
In this post, I'll share a lightweight custom hook, useMediaQuery, that allows you to detect screen sizes directly inside your React components.
A common mistake is rendering both the Mobile and Desktop components and hiding one using CSS.
{/* This is bad for performance! */}
<div className="hidden md:block"><DesktopSidebar /></div>
<div className="block md:hidden"><MobileSheet /></div>This approach bloats your DOM because React still renders both components, running their effects and logic, even if the user can't see them.
useMediaQuery HookBy using the native window.matchMedia API inside a React hook, we can track the screen state efficiently.
Here is the hook code:
import { useEffect, useState } from "react";
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
// Update state immediately if it doesn't match
if (media.matches !== matches) {
setMatches(media.matches);
}
// Listener for subsequent changes
const listener = () => setMatches(media.matches);
// Modern browsers use addEventListener for matchMedia
media.addEventListener("change", listener);
// Clean up the listener on unmount
return () => media.removeEventListener("change", listener);
}, [matches, query]);
return matches;
}window.matchMedia(query): This browser API checks if the document matches the media query string (e.g., (max-width: 768px)).useState: We store the result (true or false) in local state.change event. If the user resizes their browser window, the state updates automatically.Now, let's look at how to use this hook to solve the "Sidebar vs. Sheet" problem. We want to render a completely different UI Structure depending on the device.
import { useMediaQuery } from "./hooks/use-media-query";
export function CourseSidebar({
course,
currentLessonId,
onLessonSelect,
collapsed = false,
onToggle,
}: Props) {
const [isSheetOpen, setIsSheetOpen] = useState(false);
// Define your breakpoint here
const isMobile = useMediaQuery("(max-width: 768px)");
// 1. Render Mobile View (Sheet/Modal)
if (isMobile) {
return (
<Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
<SheetTrigger>
<Button>Menu</Button>
</SheetTrigger>
<SheetContent>
{/* Your Menu Items */}
</SheetContent>
</Sheet>
);
}
// 2. Render Desktop View (Standard Sidebar)
return (
<div className={`border-r bg-gray-100 ${collapsed ? "w-16" : "w-64"}`}>
{/* Your Menu Items */}
</div>
);
}By using if (isMobile), we are performing Conditional Rendering.
div is never rendered to the DOMSheet code is ignoredThis keeps your application lighter and faster, and it prevents weird bugs where mobile event listeners might fire while in desktop view.
CSS is for styling; Javascript is for logic. When you need to change what is rendered based on the viewport, reach for window.matchMedia. This simple useMediaQuery hook bridges the gap between your CSS breakpoints and your React component logic.