Documentation menu
Example
The same component used in the preview. Works in JSX and TSX.
import { useState } from 'react';
import { MorphDialog, MorphClose } from 'morphui';
import './demo.css';
export default function DialogDemo() {
const [created, setCreated] = useState(false);
return (
<MorphDialog
shareWords
aria-label="Create account"
panelClassName="demo-dialog-panel"
trigger={<button className="demo-primary" type="button">Create account</button>}
onOpenChange={open => { if (open) setCreated(false); }}
>
<div className="demo-sheet">
<MorphClose><button className="demo-close" type="button" aria-label="Close dialog">×</button></MorphClose>
<span className="demo-emblem" aria-hidden="true">m</span>
<h2 data-morph-words>Create account</h2>
{created ? (
<div role="status" className="demo-success">
<h3>You're all set.</h3><p>This is a local preview. No account was created or data sent.</p>
<MorphClose><button className="demo-primary" type="button">Back to the preview</button></MorphClose>
</div>
) : (
<form onSubmit={event => { event.preventDefault(); setCreated(true); }}>
<p>A small beginning. Make a little space for your next idea.</p>
<label>Email<input type="email" required placeholder="you@example.com" autoComplete="email" /></label>
<label>Password<input type="password" required minLength={8} placeholder="At least 8 characters" autoComplete="new-password" /></label>
<button className="demo-primary" type="submit">Continue</button>
<span className="demo-note">Interactive demo. Nothing is submitted.</span>
</form>
)}
</div>
</MorphDialog>
);
}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
Pass your trigger as a single React element. Set shareWords and mark the panel heading with data-morph-words to carry the label into the dialog.
The preview below the title is the packaged component. Its example and stylesheet are shown above, ready to copy into your project.
Closing the dialog
Wrap a button in MorphClose or use useMorphClose() inside the dialog’s content. Escape and clicking the backdrop use the same closing transition.
import { useMorphClose } from 'morphui';
function DoneButton() {
const close = useMorphClose();
return <button onClick={close}>Done</button>;
}
Full-screen variant
Set variant="fullscreen" to fill the viewport. The shared-word behavior is the same; the corners take longer to settle into the full-screen shape.
<MorphDialog variant="fullscreen" aria-label="Details" trigger={<button>Details</button>}>
<MorphClose><button>Close</button></MorphClose>
<h2>Details</h2>
</MorphDialog>
Accessibility
Supply a descriptive aria-label. The native dialog handles focus containment and Escape. Use a visible close button and explicit labels for form fields. Keep trigger and heading text identical when sharing words.
API reference
MorphDialog 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 | booleanfalse | 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. |
variant | 'dialog' | 'window' | 'fullscreen''dialog' | The geometry and appearance of the panel transition. |
MorphClose accepts one button as its child. The optional callback reports changes; this release does not expose a controlled open prop.