Skip to content
GetUrUI

Origin Modal

New
.md

A multi-view modal that opens from the button that launched it and returns there on close; the panel height springs between views and content crossfades through a blur.

modaldialogoverlaylayout animationshared layoutsettingsnavigation

Installation

$npx shadcn@latest add @geturui/morphing-modal
Download source (.zip)

Usage

usage.tsx
1import { MorphingModal } from "@/components/ui/morphing-modal";
2
3export function SettingsButton() {
4  return (
5    <MorphingModal
6      trigger="Settings"
7      views={[
8        {
9          id: "home",
10          title: "Settings",
11          content: (nav) => (
12            <button onClick={() => nav.go("profile")}>Edit profile</button>
13          ),
14        },
15        { id: "profile", title: "Profile", content: <ProfileForm /> },
16      ]}
17    />
18  );
19}

Props

PropTypeDefaultDescription
trigger*ReactNode—Content of the trigger button. The button itself morphs into the panel.
views*{ id: string; title: ReactNode; description?: ReactNode; content: ReactNode | (nav) => ReactNode }[]—Views the panel can show. `nav` exposes go(id), back(), close(), view and canGoBack.
initialViewstringviews[0].idView shown each time the modal opens.
openboolean—Controlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => void—Called when the modal opens or closes.
widthnumber420Panel width in px, clamped to the viewport.
triggerClassNamestring—Classes for the trigger button.
classNamestring—Classes for the panel.

Accessibility

  • The panel is a role="dialog" with aria-modal, labelled by the current view's title and described by its description.
  • Focus moves to the view heading on open and on every view change; Tab and Shift+Tab stay inside the panel.
  • Escape or a click on the backdrop closes it, and focus returns to the trigger.
  • Back and close are real buttons with labels. Under reduced motion the morph becomes a short fade and content no longer slides.
  • useMorphingModal() lets nested components navigate without prop drilling.

Related