Documentation menu
morphui Documentation

MorphDialog

The button becomes the dialog. The words stay with you.

Live preview
Press the button. Follow the motion.

Example

The same component used in the preview. Works in JSX and TSX.

MorphDialog example
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.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

PropType / defaultDescription
triggerReactElementRequiredA single element that forwards its ref to a DOM node.
childrenReactNodeRequiredContent rendered inside the panel.
shareWordsbooleanfalseConnect trigger text to a heading marked data-morph-words.
dismissOnTintClickbooleantrueClose when the surrounding backdrop is clicked.
panelClassNamestringClasses on the visible panel. Use this to set its size and appearance.
classNamestringClasses on the outer native dialog.
aria-labelstringAn accessible name for the dialog. Set this for every instance.
onOpenChange(open: boolean) => voidNotification 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.