# Auto Height

A wrapper whose height springs to fit whatever its content becomes, so swapping tabs, filtering a list or showing an error never jumps the layout below.

**Category:** Display  
**Exports:** `AutoHeight`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/auto-height.json
```
The file lands at `components/ui/auto-height.tsx`; import from `@/components/ui/auto-height`.

## API — Auto Height
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — |  |
| `stiffness` | `number` | `300` |  |
| `className` | `string` | — |  |

## Example — Auto Height

```tsx
import * as React from "react";
import { AutoHeight } from "@/components/ui/auto-height";

const tabs = {
  Short: "One line of text.",
  Medium: "A couple of sentences this time, so the box has to grow. Watch the border spring to its new height instead of jumping.",
  Long: "A longer note. Content changes all the time: a filter narrows a list, a tab swaps its panel, a form shows an error. Without an animated height the layout below snaps around. With AutoHeight the box glides to whatever its content needs, and nothing below it jumps.",
};

export default function AutoHeightDemo({ stiffness = 300 }: { stiffness?: number }) {
  const [tab, setTab] = React.useState<keyof typeof tabs>("Medium");
  return (
    <div className="w-full max-w-sm">
      <div className="mb-3 flex gap-1">
        {(Object.keys(tabs) as (keyof typeof tabs)[]).map((t) => (
          <button key={t} type="button" onClick={() => setTab(t)} className={`cursor-pointer rounded-lg px-3 py-1.5 text-sm ${tab === t ? "bg-accent font-medium" : "text-muted-foreground"}`}>
            {t}
          </button>
        ))}
      </div>
      <AutoHeight stiffness={stiffness} className="rounded-xl border bg-card">
        <p className="p-4 text-sm leading-6">{tabs[tab]}</p>
      </AutoHeight>
    </div>
  );
}
```

## Source — components/ui/auto-height.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type AutoHeightProps = {
  children: React.ReactNode;
  /** Spring stiffness; lower is lazier. */
  stiffness?: number;
  className?: string;
};

/** A wrapper whose height springs to fit whatever its content becomes, so swapping content never jumps the layout below it. */
export function AutoHeight({ children, stiffness = 300, className }: AutoHeightProps) {
  const reduce = useReducedMotion();
  const inner = React.useRef<HTMLDivElement>(null);
  const [height, setHeight] = React.useState<number | "auto">("auto");

  React.useLayoutEffect(() => {
    const el = inner.current;
    if (!el) return;
    const ro = new ResizeObserver(([e]) => setHeight(e.borderBoxSize?.[0]?.blockSize ?? el.offsetHeight));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  return (
    <motion.div className={cn("overflow-hidden", className)} animate={{ height }} transition={reduce ? { duration: 0 } : { type: "spring", stiffness, damping: 32 }}>
      <div ref={inner}>{children}</div>
    </motion.div>
  );
}
```