PParable
All components

Morph Surface

Interactive

A container that morphs in place between a compact chip and a rich panel — a single Motion layout element springs its size and corner radius while the two content states cross-fade (AnimatePresence popLayout), with a close affordance, Escape-to-collapse, and managed focus. Controlled or uncontrolled via expanded/defaultExpanded/onExpandedChange. Under prefers-reduced-motion it swaps instantly with no size, radius, or cross-fade animation.

Inspired by Cult UI — Morph / Dock Surface (free, MIT) — independently built, not affiliated. Attribution policy

Installation

npx shadcn@latest add https://parable-three.vercel.app/r/morph-surface.json

Runs the shadcn CLI — the component source is copied intocomponents/parable/morph-surface.tsx. You own it from there.

Usage

The exact code rendering the preview above.

<div style={{ display: "flex", minHeight: 340, width: "100%", alignItems: "center", justifyContent: "center", padding: 24 }}>
    <MorphSurface
      accent="#8b5cf6"
      panelLabel="Team member"
      closeLabel="Collapse profile"
      compact={
        <>
          <span style={{ display: "grid", placeItems: "center", height: 24, width: 24, borderRadius: 999, background: "linear-gradient(135deg,#8b5cf6,#ec4899)", fontSize: 11, fontWeight: 700, color: "#fff" }}>AK</span>
          <span style={{ fontWeight: 600 }}>Ada Kessler</span>
          <span style={{ display: "inline-flex", alignItems: "center", gap: 5, marginLeft: 2, fontSize: 12, color: "#22d3ee" }}>
            <span style={{ height: 6, width: 6, borderRadius: 999, background: "#22d3ee", boxShadow: "0 0 8px #22d3ee" }} />
            online
          </span>
          <ChevronRight size={15} style={{ marginLeft: 2, color: "rgba(255,255,255,0.4)" }} />
        </>
      }
      expandedContent={
        <div style={{ width: 268 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
            <span style={{ display: "grid", placeItems: "center", height: 44, width: 44, borderRadius: 999, background: "linear-gradient(135deg,#8b5cf6,#ec4899)", fontWeight: 700, color: "#fff" }}>AK</span>
            <div>
              <div style={{ fontWeight: 600, fontSize: 15 }}>Ada Kessler</div>
              <div style={{ fontSize: 12.5, color: "rgba(255,255,255,0.55)" }}>Staff Engineer · Platform</div>
            </div>
          </div>
          <p style={{ margin: "12px 0 0", fontSize: 13, lineHeight: 1.55, color: "rgba(255,255,255,0.72)" }}>
            Owns the ingestion pipeline and on-call tooling. Usually replies within a few hours.
          </p>
          <div style={{ marginTop: 14, display: "flex", gap: 8 }}>
            <span style={{ display: "inline-flex", alignItems: "center", gap: 6, borderRadius: 10, border: "1px solid rgba(255,255,255,0.12)", background: "rgba(255,255,255,0.03)", padding: "7px 11px", fontSize: 12.5, fontWeight: 500 }}>
              <MessageCircle size={14} style={{ color: "#8b5cf6" }} /> Message
            </span>
            <span style={{ display: "inline-flex", alignItems: "center", gap: 6, borderRadius: 10, border: "1px solid rgba(255,255,255,0.12)", background: "rgba(255,255,255,0.03)", padding: "7px 11px", fontSize: 12.5, fontWeight: 500 }}>
              <CalendarDays size={14} style={{ color: "#f5a623" }} /> Schedule
            </span>
          </div>
        </div>
      }
    />
  </div>

Props

MorphSurfaceProps extends PassthroughDivProps

PropTypeDefaultDescription
compact*React.ReactNodeThe collapsed "chip" content. Clicking it expands the surface.
expandedContent*React.ReactNodeThe rich "panel" content shown when expanded.
expandedbooleanControlled open state. Omit to let the component manage its own.
defaultExpandedbooleanfalseInitial open state when uncontrolled. Defaults to `false`.
onExpandedChange(expanded: boolean) => voidFires whenever the open state should change (both modes).
accentstring"#8b5cf6"Accent for the focus ring and the expanded-panel sheen. Defaults to Parable violet `#8b5cf6` (mirrors the site's `--pb-violet` token).
panelLabelstring"Details"Accessible name for the expanded panel group. Defaults to "Details".
closeLabelstring"Collapse"Accessible label for the collapse control. Defaults to "Collapse".

npm dependencies

  • motion
  • lucide-react