useMediaQuery
Tracks whether a CSS media query currently matches, safely on the server and the client.
Built on useSyncExternalStore, so there is no render-then-correct flash on mount.
Install
$
npx shadcn@latest add @vibra/use-media-queryNeeds the @vibra registry in your components.json — set it up once.
Examples
"use client"
import { type ComponentType } from "react"
import { MonitorIcon, SmartphoneIcon, TabletIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { useMediaQuery } from "@/hooks/use-media-query"
const BREAKPOINTS: { label: string; query: string; icon: ComponentType<{ className?: string }> }[] = [
{ label: "Mobile", query: "(max-width: 639px)", icon: SmartphoneIcon },
{ label: "Tablet", query: "(min-width: 640px) and (max-width: 1023px)", icon: TabletIcon },
{ label: "Desktop", query: "(min-width: 1024px)", icon: MonitorIcon },
]
function BreakpointRow({
label,
query,
icon: Icon,
}: {
label: string
query: string
icon: ComponentType<{ className?: string }>
}) {
const matches = useMediaQuery(query)
return (
<div className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
<span className="flex items-center gap-2 text-foreground">
<Icon className="size-4 text-muted-foreground" />
{label}
</span>
<Badge variant={matches ? "default" : "outline"}>{matches ? "active" : "—"}</Badge>
</div>
)
}
export default function UseMediaQueryDemo() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Viewport</CardTitle>
<CardDescription>Live matchMedia — resize the window to see it change.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2">
{BREAKPOINTS.map((breakpoint) => (
<BreakpointRow key={breakpoint.query} {...breakpoint} />
))}
</CardContent>
</Card>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| query | string | — | CSS media query to watch, e.g. "(min-width: 768px)". |
| returns | boolean | — | Whether the query currently matches; false on the server and before the first client read. |
Dependencies
Registry
Source
import * as React from "react"
/** The live MediaQueryList for `query`, or `undefined` when matchMedia isn't available (SSR, old browsers, tests). */
function getMediaQueryList(query: string): MediaQueryList | undefined {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return undefined
return window.matchMedia(query)
}
/** Tracks whether a CSS media query matches; false on the server and until matchMedia is read, then synced via the "change" event through useSyncExternalStore. */
export function useMediaQuery(query: string): boolean {
const subscribe = React.useCallback(
(onStoreChange: () => void) => {
const mql = getMediaQueryList(query)
if (!mql) return () => {}
mql.addEventListener("change", onStoreChange)
return () => mql.removeEventListener("change", onStoreChange)
},
[query]
)
const getSnapshot = React.useCallback(() => getMediaQueryList(query)?.matches ?? false, [query])
const getServerSnapshot = () => false
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
}