caliper uidocs

Auto height

Free

A container that follows its content's height, so a wizard step, a validation summary or a details panel grows and shrinks instead of snapping. The content changes at once.

Installs components/ui/auto-height.tsxDepends on none beyond ReactMotion shared motion tokens
pnpm dlx shadcn@latest add https://caliperui.site/r/auto-height.json

When to use #

Use it for

  • Content that changes length after an action: a step, an error list, a toggle that adds fields.
  • Cards whose body swaps between states.

Skip it when

  • Content that changes while people read it, such as streaming text.
  • Accordions; the accordion handles its own height.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/auto-height.json

Adds its 4 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { Check } from "lucide-react"
import { useState } from "react"
import { AutoHeight } from "@/components/ui/auto-height"
import { Button } from "@/components/ui/button"
import { Field } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { SegmentedControl } from "@/components/ui/segmented-control"

export function AutoHeightDemo() {
  const [kind, setKind] = useState("card");
  const [errors, setErrors] = useState<string[]>([]);
  return (
    <div className="grid w-full max-w-md gap-4">
      <SegmentedControl label="Payment method" value={kind} onValueChange={setKind} options={[{ value: "card", label: "Card" }, { value: "bank", label: "Bank transfer" }, { value: "invoice", label: "Invoice" }]} />
      <AutoHeight className="rounded-surface border border-border bg-surface">
        <div className="grid gap-3 p-4">
          {kind === "card" && (
            <>
              <Field label="Card number">{(field) => <Input {...field} inputMode="numeric" placeholder="4242 4242 4242 4242" />}</Field>
              <div className="grid grid-cols-2 gap-3">
                <Field label="Expiry">{(field) => <Input {...field} placeholder="MM / YY" />}</Field>
                <Field label="CVC">{(field) => <Input {...field} inputMode="numeric" placeholder="123" />}</Field>
              </div>
            </>
          )}
          {kind === "bank" && <Field label="IBAN">{(field) => <Input {...field} placeholder="DE89 3704 0044 0532 0130 00" />}</Field>}
          {kind === "invoice" && <p className="text-sm text-muted-foreground">We email an invoice on the 1st, payable within 30 days.</p>}
          {errors.length > 0 && (
            <ul className="grid gap-1 rounded-control bg-destructive-subtle px-3 py-2 text-sm text-destructive" role="alert">
              {errors.map((e) => <li key={e}>{e}</li>)}
            </ul>
          )}
          <div className="flex gap-2">
            <Button size="sm" onClick={() => setErrors(kind === "invoice" ? [] : ["Enter the full number.", "Check the expiry date."].slice(0, kind === "card" ? 2 : 1))}>Check details</Button>
            {errors.length > 0 && <Button size="sm" variant="ghost" onClick={() => setErrors([])}>Clear</Button>}
          </div>
        </div>
      </AutoHeight>
    </div>
  );
}

API reference #

AutoHeight

component

No props of its own; it accepts the native props of its root element.

Accessibility #

It is built on native elements, so roles and focus come from the browser.

No custom key handling; Tab and the native activation keys do the work.

Motion #

Transitions use the shared duration and easing tokens. Under prefers-reduced-motion those tokens collapse to instant and travel distances go to zero, so state changes still happen without movement.