PParable
All components

Family Drawer

Interactive

A bottom sheet that springs up from the edge and morphs its height through a family of steps: swap the children (with an optional contentKey) and it measures the incoming content, springs the panel to fit, and cross-fades the step swap, while a grabber handle drags the sheet down to dismiss (velocity-aware). Fully modal — focus trap and return, body scroll lock, Escape and backdrop to close; under prefers-reduced-motion the height morph, slide-up, and drag physics are dropped for an instant, legible show/hide.

Inspired by Emil Kowalski / Vaul-style drawer (upstream of Skiper); pattern — independently built, not affiliated. Attribution policy

Height morphs as steps change

Installation

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

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

Usage

The exact code rendering the preview above.

<div className="flex w-full flex-col items-center gap-6 px-6 py-8">
    <div className="flex items-center gap-1.5" aria-hidden>
      <span className="h-1.5 w-6 rounded-full bg-violet-400" />
      <span className="h-1.5 w-6 rounded-full bg-white/20" />
      <span className="h-1.5 w-6 rounded-full bg-white/20" />
    </div>
    <FamilyDrawer
      triggerLabel={
        <>
          <Wallet className="size-4" aria-hidden />
          Add payment method
        </>
      }
      title="Add a card"
      description="Enter details, review, then confirm — the sheet morphs between steps."
      contentKey="step-1"
    >
      <div className="space-y-2.5 text-sm text-white/70">
        <div className="rounded-xl bg-white/[0.04] px-3 py-2.5 ring-1 ring-white/10">
          Card number
        </div>
        <div className="grid grid-cols-2 gap-2.5">
          <div className="rounded-xl bg-white/[0.04] px-3 py-2.5 ring-1 ring-white/10">
            Expiry
          </div>
          <div className="rounded-xl bg-white/[0.04] px-3 py-2.5 ring-1 ring-white/10">
            CVC
          </div>
        </div>
        <button
          type="button"
          className="mt-1 w-full rounded-xl bg-violet-500 px-4 py-2.5 text-sm font-semibold text-white transition-[filter] hover:brightness-110"
        >
          Continue
        </button>
      </div>
    </FamilyDrawer>
    <div className="flex items-center gap-2 text-xs text-white/40">
      <Layers className="size-3.5" aria-hidden />
      Height morphs as steps change
    </div>
  </div>

Props

FamilyDrawerProps extends Omit< React.HTMLAttributes<HTMLDivElement>, | "children" | "title" | "onAnimationStart" | "onAnimationEnd" | "onAnimationIteration" | "onDrag" | "onDragStart" | "onDragEnd" >

PropTypeDefaultDescription
openbooleanControlled open state. Pair with `onOpenChange`.
defaultOpenbooleanfalseUncontrolled initial open state.
onOpenChange(open: boolean) => voidFires with the next open state on trigger, backdrop, Escape, or drag-dismiss.
triggerLabelReact.ReactNodeRenders a built-in trigger button with this label. Omit to open via `open`.
children*React.ReactNodeDrawer body. Swap it to move between steps — the height springs to the new content. Change `contentKey` alongside it to cross-fade the swap.
contentKeyReact.KeyIdentity of the current step. When it changes, the outgoing content cross-fades out as the incoming content fades in and the height morphs. Leave undefined to update children in place (height still morphs).
titleReact.ReactNodeAccessible + visible heading. When set, labels the dialog via `aria-labelledby`.
descriptionReact.ReactNodeSecondary line under the title; describes the dialog via `aria-describedby`.
accentstring"#8b5cf6"Accent hex — trigger, grabber, focus ring. Mirrors the site `--pb-violet` token.
backgroundstring"#0f0f10"Panel face colour. Mirrors the site `--pb-ink` token.
dismissThresholdnumber120Downward drag distance (px) past which release dismisses the drawer.

npm dependencies

  • motion