Documentation menu
Example
The same component used in the preview. Works in JSX and TSX.
import { MorphWindow, MorphClose } from 'morphui';
import './demo.css';
export default function WindowDemo() {
return (
<MorphWindow
aria-label="A little context"
panelClassName="demo-window-panel"
trigger={<button className="demo-chip" type="button">A little context</button>}
>
<div className="demo-sheet">
<MorphClose><button className="demo-close" type="button" aria-label="Close window">×</button></MorphClose>
<h2 data-morph-words>A little context</h2>
<p>Some things need a little more room.</p>
<p>This window grows from the button you pressed. The words come along, the details come into focus, and everything finds its way back when you close it.</p>
<div className="demo-detail"><span>Opening</span><span>700 ms</span></div>
<div className="demo-detail"><span>Closing</span><span>500 ms</span></div>
<MorphClose><button className="demo-primary" type="button">Got it</button></MorphClose>
</div>
</MorphWindow>
);
}Preview stylesheet demo.css
.demo-primary,.demo-chip,.demo-card,.demo-close { font:inherit; cursor:pointer; border:0; }
.demo-primary { display:inline-flex; align-items:center; justify-content:center; min-height:48px; padding:0 25px; border-radius:999px; background:var(--morph-accent); color:var(--morph-on-accent); font-weight:500; font-size:15px; white-space:nowrap; }
.demo-chip { padding:14px 22px; border-radius:16px; background:var(--morph-chip); color:var(--morph-text); font-size:15px; }
.demo-primary:active,.demo-chip:active { transform:scale(.98); }
.demo-dialog-panel { width:min(460px,100%); height:min(650px,100%); border-radius:34px; }
.demo-window-panel { width:min(530px,100%); height:min(570px,100%); border-radius:34px; }
.demo-sheet { padding:60px 36px 32px; display:flex; flex-direction:column; gap:20px; }
.demo-sheet h2 { font-size:30px; line-height:1.2; letter-spacing:-.025em; margin:0; font-weight:550; }
.demo-sheet p { margin:0; color:var(--morph-muted); font-size:15px; line-height:1.65; }
.demo-sheet form { display:flex; flex-direction:column; gap:20px; }
.demo-sheet label { display:flex; flex-direction:column; gap:8px; font-size:14px; }
.demo-sheet input { width:100%; height:48px; padding:0 14px; color:var(--morph-text); background:var(--morph-surface); border:1px solid var(--morph-border); border-radius:10px; font:inherit; }
.demo-sheet input::placeholder { color:var(--morph-muted); }
.demo-emblem { display:grid; place-items:center; width:44px; height:44px; border-radius:16px; background:var(--morph-accent-soft); color:var(--morph-accent); font-size:26px; font-weight:600; }
.demo-close { position:absolute; top:18px; right:18px; display:grid; place-items:center; width:38px; height:38px; border-radius:50%; background:var(--morph-raised); color:var(--morph-text); font-size:23px; line-height:1; z-index:2; }
.demo-note { font-size:12px; color:var(--morph-muted); }
.demo-detail { display:flex; justify-content:space-between; padding:12px 0; border-bottom:1px solid var(--morph-border); font-size:14px; }
.demo-success { display:flex; flex-direction:column; gap:24px; }
.demo-success h3 { margin:0; font-size:24px; }
.demo-card { width:240px; padding:0; overflow:hidden; border-radius:24px; background:var(--morph-bg); color:var(--morph-text); text-align:left; box-shadow:0 8px 28px rgb(0 0 0 / .06); }
.demo-art { display:block; width:100%; height:200px; background:radial-gradient(circle at 66% 74%,var(--morph-accent) 0 26%,transparent 26.4%),radial-gradient(circle at 66% 74%,transparent 0 36%,color-mix(in srgb,var(--morph-accent) 34%,transparent) 36.4% 37.6%,transparent 38%),radial-gradient(circle at 26% 26%,var(--morph-muted) 0 7%,transparent 7.4%),var(--morph-raised); }
.demo-card-caption { display:flex; align-items:center; justify-content:space-between; padding:17px 19px; font-size:13px; }
.demo-card-caption span { font-size:18px; }
.demo-art-wide { height:46dvh; }
.demo-story { max-width:680px; margin:0 auto; padding:40px 28px 80px; }
.demo-story h2 { font-size:clamp(32px,5vw,52px); line-height:1.1; letter-spacing:-.03em; margin:18px 0 24px; }
.demo-story p:not(.demo-note) { color:var(--morph-muted); font-size:17px; line-height:1.7; margin:0 0 24px; }
.demo-feature { min-height:100%; }
@media(max-width:480px) { .demo-sheet { padding:66px 24px 28px; } .demo-dialog-panel { height:min(640px,100%); } }Usage
MorphWindow shares the trigger’s label with a heading marked data-morph-words. Word sharing is enabled by default. The window preserves its own blur treatment and closing curve.
Use panelClassName to size the window. Its position remains centered in the viewport while its animation begins at the trigger.
A persistent trigger
To use the original unshared variant, pass shareWords={false}. The trigger stays visible as the window expands from its position. With no shared elements, the window keeps its own surface.
<MorphWindow
shareWords={false}
aria-label="More information"
trigger={<button>What is this?</button>}
>
<h2>More information</h2>
<MorphClose><button>Got it</button></MorphClose>
</MorphWindow>
Closing and focus
Use MorphClose, Escape, or the backdrop. The label follows its return path while the surrounding content softens and closes. Always provide a visible close action and an accessible dialog name.
API reference
MorphWindow props
| Prop | Type / default | Description |
|---|---|---|
trigger | ReactElementRequired | A single element that forwards its ref to a DOM node. |
children | ReactNodeRequired | Content rendered inside the panel. |
shareWords | booleantrue | Connect trigger text to a heading marked data-morph-words. |
dismissOnTintClick | booleantrue | Close when the surrounding backdrop is clicked. |
panelClassName | string— | Classes on the visible panel. Use this to set its size and appearance. |
className | string— | Classes on the outer native dialog. |
aria-label | string— | An accessible name for the dialog. Set this for every instance. |
onOpenChange | (open: boolean) => void— | Notification when opening starts and closing finishes. |
MorphClose accepts one button as its child. The optional callback reports changes; this release does not expose a controlled open prop.