caliper uidocs

Morph surface

Free

A floating surface that grows out of the element that opened it.

Installs components/ui/morph-surface.tsxDepends on none beyond ReactMotion honours reduced motion
pnpm dlx shadcn@latest add https://caliperui.site/r/morph-surface.json

When to use #

Use it for

  • Building your own menus, pickers and cards that should feel attached to their trigger.
  • You need the shared overlay frost and edge.

Skip it when

  • A standard menu or popover already fits.

Installation #

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

Adds its 2 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 { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { MorphSurface } from "@/components/ui/morph-surface"

export function OriginSurface() {
  const origin = useRef<HTMLButtonElement>(null);
  const [open, setOpen] = useState(false);
  return (
    <div className="pb-24">
      <Button ref={origin} variant="secondary" onClick={() => setOpen((value) => !value)}>
        {open ? "Close" : "Open surface"}
      </Button>
      <MorphSurface open={open} originRef={origin} width={240}>
        <p className="p-4 text-sm">This surface grows from the button that opened it.</p>
      </MorphSurface>
    </div>
  );
}

API reference #

MorphSurface

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
open *boolean—
originRef *RefObject<HTMLElement | null>—
placement"center" | "below""below""below" opens under the origin; "center" opens in the middle of the screen near the top.
widthnumber | "origin"280Pixels, or "origin" to match the origin's width at least.
gapnumber6
radiusnumber12
onExited(() => void)—

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 #

It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.