MorphingPopover

Morph a trigger element into a centred dialog panel using a shared layout animation.

MorphingPopover is a client component that uses a shared motion/react layoutId to animate a trigger into a panel. Use it for short, focused tasks started from a single button, such as sharing or renaming. The panel is portalled to document.body and centred in a fixed full-screen overlay, rather than anchored to the trigger.

Import#

typescript
import { MorphingPopover } from "@reactberry/system/blocks";

Usage#

typescript
import { MorphingPopover } from "@reactberry/system/blocks";
import { Box, Button, Text } from "@reactberry/system/elements";

export default function ShareAction() {
  return (
    <MorphingPopover
      trigger={
        <Button variant="primary" shape="pill">
          <Text>Share</Text>
        </Button>
      }
      panelProps={{ width: "20rem", p: "medium" }}
    >
      {({ close }) => (
        <Box display="flex" flexDirection="column" gap="small">
          <Text fontWeight="600">Share this page</Text>
          <Button variant="ghost" onClick={close}>
            <Text>Done</Text>
          </Button>
        </Box>
      )}
    </MorphingPopover>
  );
}

Examples#

Controlled#

Pass open and onOpenChange to own the state, for example to reset a draft each time the panel opens or to close it after saving.

Website redesign

Custom trigger#

renderTrigger receives an onClick that opens the panel. Wire it to your element yourself; it is not called automatically.

API#

MorphingPopover#

PropTypeDefaultDescription
trigger
ReactNode
—

Trigger element shown when the popover is closed.

triggerProps
Record<string, any>
—

Props spread onto the wrapper Box. Used only when the trigger (or the renderTrigger result) is not a valid React element.

placement
string
bottom end

Optional placement hint (kept for API compatibility; currently ignored).

panelProps
Record<string, any>
—

Props forwarded to the morphing panel container.

containerProps
Record<string, any>
—

Props forwarded to the outer container Box.

renderTrigger
((props: MorphingPopoverRenderTriggerProps) => ReactNode)
—

Custom trigger renderer for full control over the trigger element. Signature mirrors the standard Popover API.

children
ReactNode | ((context: { close: () => void; }) => ReactNode)
—

Popover content. If a function, receives a close() helper.

transition
Transition
{…

Motion transition applied via MotionConfig.

variants
Variants
{…

Framer Motion variants for the morphing panel.

defaultOpen
boolean
false

Uncontrolled initial open state.

open
boolean
—

Controlled open state.

onOpenChange
((open: boolean) => void)
—

Callback when open state changes.

A valid trigger element is recreated with motion.create and given the shared layoutId. Clicking it calls its own onClick and then opens the panel unless event.preventDefault() was called. panelProps override the panel defaults (skin="surface", shape="rounded", p="xsmall", $shadow="medium"); containerProps override the outer Box defaults (display="inline-flex", position="relative"). Remaining props are not spread.

Keyboard#

KeyAction
Escape

Closes the panel while it is open. The listener is on `document`.

Accessibility#

  • The panel has role="dialog" and aria-modal="true", but focus is not moved into or trapped within it, and is not returned to the trigger on close.
  • The panel has no accessible name; pass aria-label or aria-labelledby through panelProps.
  • The trigger gets no aria-expanded or aria-controls.

Notes#

  • A mousedown or touchstart outside the panel closes it.
  • The overlay uses zIndex="999999". A transparent Underlay behind the panel blurs the page (12px backdrop blur, masked to fade out toward the bottom).
  • The trigger remains mounted while the panel is open; the shared layoutId drives the morph between them.