# useResizeObserver

A hook that measures an element and updates on resize.

## Installation

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

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

## Preview

```tsx
import { useResizeObserver } from "@/hooks/use-resize-observer";

export function Preview() {
  const { ref, width, height } = useResizeObserver<HTMLDivElement>();

  return (
    <div className="flex w-full max-w-sm flex-col gap-3 text-sm">
      <div
        ref={ref}
        className="grid min-h-24 w-56 max-w-full min-w-24 resize place-items-center overflow-auto rounded-lg border border-dashed border-border bg-muted/40 p-3"
      >
        <span className="text-xs text-muted-foreground">Drag the corner ↘</span>
      </div>

      <p className="font-mono text-xs text-muted-foreground">
        {width} × {height}
      </p>
    </div>
  );
}
```


## Source

### hooks/use-resize-observer.ts

```ts
"use client";

import * as React from "react";

type ResizeObserverBox = "border-box" | "content-box";

type UseResizeObserverOptions = {
  box?: ResizeObserverBox;
  round?: (value: number) => number;
  onResize?: (size: ElementSize) => void;
};

type ElementSize = {
  width: number;
  height: number;
};

type UseResizeObserverResult<T extends Element> = ElementSize & {
  ref: (node: T | null) => void;
  element: T | null;
};

function useResizeObserver<T extends Element = HTMLElement>({
  box = "content-box",
  round = Math.round,
  onResize,
}: UseResizeObserverOptions = {}): UseResizeObserverResult<T> {
  const [element, setElement] = React.useState<T | null>(null);
  const [size, setSize] = React.useState<ElementSize>({ width: 0, height: 0 });

  const roundRef = React.useRef(round);
  const onResizeRef = React.useRef(onResize);

  React.useEffect(() => {
    roundRef.current = round;
    onResizeRef.current = onResize;
  });

  React.useEffect(() => {
    if (!element) return undefined;
    if (typeof ResizeObserver === "undefined") return undefined;

    const observer = new ResizeObserver(([entry]) => {
      if (!entry) return;

      const next = readEntrySize(entry, box, roundRef.current);

      setSize((previous) =>
        previous.width === next.width && previous.height === next.height ? previous : next,
      );
      onResizeRef.current?.(next);
    });

    observer.observe(element, { box });
    return () => observer.disconnect();
  }, [element, box]);

  return { ref: setElement, element, width: size.width, height: size.height };
}

function readEntrySize(
  entry: ResizeObserverEntry,
  box: ResizeObserverBox,
  round: (value: number) => number,
): ElementSize {
  const boxSize = box === "border-box" ? entry.borderBoxSize : entry.contentBoxSize;
  const measurement = Array.isArray(boxSize) ? boxSize[0] : boxSize;

  if (measurement) {
    return { width: round(measurement.inlineSize), height: round(measurement.blockSize) };
  }

  return { width: round(entry.contentRect.width), height: round(entry.contentRect.height) };
}

export {
  useResizeObserver,
  type ElementSize,
  type UseResizeObserverOptions,
  type UseResizeObserverResult,
};
```



## Usage

Element size, not viewport size. Use it for the decisions a media query cannot make: a card that
switches layout because its column is narrow, a chart that needs pixel dimensions, a textarea that
must re-measure when its container changes width.

```tsx
import { useResizeObserver } from "@/hooks/use-resize-observer";

function Chart({ points }: { points: Point[] }) {
  const { ref, width, height } = useResizeObserver<HTMLDivElement>();

  return (
    <div ref={ref} className="h-64 w-full">
      {width > 0 ? <svg width={width} height={height}>{/* … */}</svg> : null}
    </div>
  );
}
```

`ref` is a callback ref, so the measurement starts as soon as the node attaches — including when the
element mounts later or is swapped out.

## Boxes

`content-box` (the default) excludes padding and border; `border-box` is the element's rendered
size. Pick `border-box` when the number feeds back into a layout, so padding changes do not cause a
measure-resize loop.

```tsx
const { ref, width } = useResizeObserver<HTMLDivElement>({ box: "border-box" });
```

## Options

| Option     | Description                                                                     |
| ---------- | ------------------------------------------------------------------------------- |
| `box`      | `"content-box"` (default) or `"border-box"`.                                     |
| `round`    | How subpixel values are collapsed. Defaults to `Math.round`; pass `Math.floor` to avoid overflow, or `(n) => n` for exact values. |
| `onResize` | Called on every observed change, for work that should not trigger a re-render.   |

## Behavior

- State updates are skipped when the rounded size did not change, so a resize that lands on the same
  integer does not re-render.
- `onResize` is read from a ref and never re-subscribes the observer.
- Before the first measurement — and during server rendering — `width` and `height` are `0`. Branch
  on that rather than rendering a zero-sized canvas.

