# useMediaQuery

A hook that tracks a CSS media query without tearing during hydration.

## Installation

```bash
npx shadcn@latest add https://mwui.vercel.app/r/use-media-query.json
```

[Registry JSON](https://mwui.vercel.app/r/use-media-query.json)

## Preview

```tsx
import { useMediaQuery } from "@/hooks/use-media-query";

const QUERIES = [
  { label: "sm", query: "(min-width: 640px)" },
  { label: "md", query: "(min-width: 768px)" },
  { label: "lg", query: "(min-width: 1024px)" },
  { label: "Dark", query: "(prefers-color-scheme: dark)" },
  { label: "Reduced motion", query: "(prefers-reduced-motion: reduce)" },
];

function QueryRow({ label, query }: { label: string; query: string }) {
  const matches = useMediaQuery(query);

  return (
    <div className="flex items-center justify-between gap-6 text-sm">
      <span className="font-mono text-xs text-muted-foreground">{label}</span>
      <span
        data-matches={matches || undefined}
        className="rounded-md bg-muted px-2 py-0.5 text-xs data-matches:bg-primary data-matches:text-primary-foreground"
      >
        {matches ? "matches" : "no match"}
      </span>
    </div>
  );
}

export function Preview() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      {QUERIES.map((entry) => (
        <QueryRow key={entry.query} label={entry.label} query={entry.query} />
      ))}
      <p className="pt-2 text-xs text-muted-foreground">Resize the window to see these change.</p>
    </div>
  );
}
```


## Source

### hooks/use-media-query.ts

```ts
"use client";

import * as React from "react";

type UseMediaQueryOptions = {
  defaultValue?: boolean;
};

function useMediaQuery(
  query: string,
  { defaultValue = false }: UseMediaQueryOptions = {},
): boolean {
  const subscribe = React.useCallback(
    (onStoreChange: () => void) => {
      const mediaQueryList = window.matchMedia(query);
      mediaQueryList.addEventListener("change", onStoreChange);
      return () => mediaQueryList.removeEventListener("change", onStoreChange);
    },
    [query],
  );

  const getSnapshot = React.useCallback(() => window.matchMedia(query).matches, [query]);
  const getServerSnapshot = React.useCallback(() => defaultValue, [defaultValue]);

  return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

export { useMediaQuery, type UseMediaQueryOptions };
```



## Usage

Subscribes to a CSS media query and re-renders when it starts or stops matching.

```tsx
import { useMediaQuery } from "@/hooks/use-media-query";

function Navigation() {
  const isDesktop = useMediaQuery("(min-width: 768px)");

  return isDesktop ? <Sidebar /> : <MobileDrawer />;
}
```

## Server rendering

The hook is built on `useSyncExternalStore`, which takes a separate server snapshot. On the server
and during the first client render it returns `defaultValue`, then switches to the real result — so
React never has to reconcile two different trees for the same commit.

```tsx
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)", {
  defaultValue: true,
});
```

Pick the `defaultValue` that matches the markup you render on the server. For layout queries that
usually means the mobile branch; for `prefers-reduced-motion` it usually means the safe one.

> **Prefer CSS where you can.** A media query in CSS costs nothing and never mismatches. Reach for
> this hook when the breakpoint has to change *what renders* — a different component, a different
> DOM structure — not just how it looks.

## Notes

- The query string is passed straight to `window.matchMedia`, so any valid media query works.
- Changing `query` resubscribes automatically.
- Listeners are registered with `addEventListener`, and are cleaned up on unmount.

