# Morphing Popover

> A Liquid Glass pill that swells into a panel holding anything: a form, a picker, a note.

- Page: https://ui.devigner.cc/components/morphing-popover
- Category: Interface
- Requirements: React 18 or 19, Tailwind CSS v4
- Install: `npx devignerui add morphing-popover` (writes components/ui/morphing-popover.tsx)
- Packages it needs: cn, motion, devignerui

## What the demo shows

The live demo is an Add note pill on the dot field. Click it and the glass swells into a note form with Cancel and Save note; saving melts it back into the pill and shows Note saved underneath. On the component page the preview opens on the shader glass over a night photo, with Settings to switch back to the page lens.

## When to use it

- A small task that belongs next to its trigger: add a note, rename, share, set a reminder.
- Anywhere Liquid Glass is the look and a plain popover would break it.

## When to reach for something else

- Long forms or anything that needs scrolling. Use a dialog or a page.
- A list of actions. Use Liquid Glass's menu, which handles arrow keys between rows.

## Usage

```tsx
import { MorphingPopover } from "@/components/ui/morphing-popover";

<MorphingPopover label="Add note">{(close) => <NoteForm onDone={close} />}</MorphingPopover>
```

Custom composition:

```tsx
<MorphingPopover label="Share" icon={<IconShare />} transparency={0.6}>{panel}</MorphingPopover>
```

### A rename form that closes on save

Use the render prop to close the panel from the form's own buttons.

```tsx
import { MorphingPopover } from "@/components/ui/morphing-popover";

export function Rename({ name, onRename }: { name: string; onRename: (next: string) => void }) {
  return (
    <MorphingPopover label="Rename">
      {(close) => (
        <form
          className="flex w-60 flex-col gap-2"
          onSubmit={(e) => {
            e.preventDefault();
            const next = new FormData(e.currentTarget).get("name");
            if (typeof next === "string" && next.trim()) onRename(next.trim());
            close();
          }}
        >
          <input name="name" defaultValue={name} aria-label="Name" className="rounded-xl px-3 py-2" />
          <button type="submit">Save</button>
        </form>
      )}
    </MorphingPopover>
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| label | `string` | - | Text on the closed pill, and the panel's accessible name. |
| icon | `ReactNode` | - | Shown before the label on the pill. |
| children | `ReactNode \| (close) => ReactNode` | - | What the panel holds. Pass a function to get close, for a Cancel or Save button inside. |
| open | `boolean` | - | Controlled open state. Pass with onOpenChange. |
| defaultOpen | `boolean` | false | Open on first render when uncontrolled. |
| onOpenChange | `(open: boolean) => void` | - | Called when the panel opens or closes. |
| transparency | `number` | 0.05 | How tinted the glass is, from 0 (ultra clear) to 1 (fully tinted). |
| backdrop | `GlassBackdrop` | - | Draws the glass with a WebGL shader instead: { src?, area }, an image and a ref to the element it fills like object-fit: cover. transparency tints it the same way. |
| blur | `number` | 4 | How far the shader glass blurs the backdrop, in px. Only with backdrop. |
| disabled | `boolean` | false | Disables the pill and closes the panel. |
| classNames | `MorphingPopoverClassNames` | - | Slot classes: trigger (the label and icon on the pill) and panel (the box around the content). |

## Keyboard

| Keys | Action |
| --- | --- |
| Enter / Space | Opens the panel from the pill and moves focus to its first field or button. |
| Tab | Moves through the panel. Tabbing out of it closes it. |
| Escape | Closes the panel and returns focus to the pill. |

## Accessibility

- The pill is a button with aria-haspopup="dialog" and aria-expanded; the panel is a non-modal dialog named by label.
- Closed, the panel is inert, so nothing in it can be focused or read until it opens.
- Closing with Escape or through close returns focus to the pill.
- Under prefers-reduced-transparency the glass turns into a solid tint.
- Honors prefers-reduced-motion and an ancestor <MotionConfig reducedMotion="always">, so an in-app motion switch works too.

## Theming

- The glass is the same as Liquid Glass's: transparency runs from ultra clear (0) to fully tinted (1), and the tint is your background color.
- Only Chromium bends the page through the rim; Safari and Firefox get frosted glass.
- With backdrop the glass is drawn by the same WebGL shader as Liquid Glass's, over that image instead of the page, and it works in every browser with WebGL. blur sets how far it softens the image.
- It swells open and melts back on TWEEN_GLASS_SOFT, a sampled spring about 6% past its size that settles in 1s.
- classNames.trigger styles the pill's label and icon, and classNames.panel the box around the content, which pads 16px.
- The panel's text uses foreground. Style the content inside it like any other markup.

## Edge cases

- The pill fits its label and the panel fits its content, measured again whenever either changes size.
- The panel swells out from the pill's center, as far each way, so leave room around the pill or place it where an overlap is fine.
- A press anywhere outside closes the panel without moving focus.
- disabled closes an open panel; enabling it again doesn't reopen it.

## Troubleshooting

**The glass looks frosted, not bent.**
Only Chromium renders the lens. Safari and Firefox get frosted glass by design, and so does anyone with reduced transparency on.

**The panel is cut off.**
It grows past the pill's box on every side. Remove overflow-hidden from the pill's ancestors, or give the pill room.

**The component renders with no background or the wrong colors.**
Colors use the standard shadcn/ui tokens only (background, foreground, primary, secondary, muted, accent, border, input, ring, destructive), so a project set up with shadcn/ui needs nothing extra and the component follows its theme, dark mode included. Without shadcn/ui, define those CSS variables for :root and .dark and map them in your Tailwind v4 @theme, or run npx shadcn init.
