Skip to content
Template Empire
Browse components

Morph Trigger Modal

A free morphing modal react component whose surface expands out of its own trigger.

Changelog

Updated today

LatestVersion 4.2

Reports that send themselves

Saved reports now run on a schedule, and dashboards with fifty charts stop feeling like fifty charts.

4.1.2

Webhook retries no longer double-send digest emails.

4.1

Shared dashboards, comment threads on charts, and sign-in with Okta across every plan.

Customise

560px
28px
340
0.58
8px

Code

<ModalMorphTrigger triggerLabel="View release details" title="Release 4.2 is available" />

Morph Trigger Modal is a free morphing modal react component for Next.js. One shared-layout surface carries the trigger's geometry to the dialog's on a spring, and the body fades in once that surface has started expanding rather than arriving stretched with it, so the reader sees a shape open and then fill.

The dialog itself is a native dialog element opened with showModal(), which means the focus trap, the top layer, Escape and making the rest of the page inert all come from the platform rather than from this component. Focus returns the instant closing begins, not when the exit animation ends, so a Tab pressed during the morph starts from the trigger rather than the top of the document. Background scroll is locked through the library's shared reference count, so two overlapping overlays coordinate instead of fighting over the body's inline styles, and with reduced motion enabled the morph and the backdrop blur are dropped for a plain fade while every one of those behaviours is unchanged.

Install

Add the @te registry to your components.json once:

{
  "registries": {
    "@te": "https://templateempire.io/r/{name}.json"
  }
}

Then install the component:

npx shadcn@latest add @te/modal-morph-trigger

Usage

"use client";

import { ModalMorphTrigger } from "@/components/modal-morph-trigger";

export default function Page() {
  return (
    <ModalMorphTrigger
      triggerLabel="View release details"
      title="Release 4.2 is available"
      description="Three changes worth reading before you upgrade."
    >
      <p>Everything you want in the dialog body goes here.</p>
    </ModalMorphTrigger>
  );
}

Props

PropTypeDefaultDescription
triggerLabelstringrequiredThe trigger button's visible text, which is also its accessible name.
titlestringrequiredThe dialog's heading, and the accessible name of the dialog.
descriptionstringundefinedOptional supporting line. When present it is associated through aria-describedby; when absent no dangling reference is left behind.
childrenReactNoderequiredThe dialog body.
openbooleanundefinedControlled open state. When supplied the component keeps no state of its own, so a parent that declines a request is never overruled.
defaultOpenbooleanfalseUncontrolled initial state only. Ignored when open is provided.
onOpenChange(open: boolean) => voidundefinedCalled after a user open or close request, with the state being asked for. Fires once per request, including Escape and a backdrop press.
widthnumber560Dialog maximum width in CSS pixels, clamped to 320-760.
radiusnumber28Trigger and dialog corner radius in CSS pixels, clamped to 0-48. One value for both, because it is one surface.
springStiffnessnumber340Stiffness of the morph spring, clamped to 140-560. Higher is snappier.
overlayOpacitynumber0.58Backdrop opacity, clamped to 0.2-0.82.
overlayBlurnumber8Backdrop blur in CSS pixels, clamped to 0-18. Dropped entirely under reduced motion.
classNamestringundefinedAdditional classes merged onto the component wrapper with cn().

Customisation

Morph Trigger Modal declares three tokens of its own, each falling back to a global semantic token, so it retints from an ancestor without touching the component source. Geometry and timing come from props rather than tokens, because they are clamped: --morph-modal-accent colours the focus rings, --morph-modal-panel is the one surface shared by the trigger and the dialog, and --morph-modal-overlay is the backdrop colour before opacity is applied.

:root {
  --morph-modal-accent: var(--chart-2);
  --morph-modal-panel: var(--card);
  --morph-modal-overlay: oklch(0.12 0.02 260);
}

<ModalMorphTrigger triggerLabel="Open" title="Details" width={420} radius={12}>
  <p>Body</p>
</ModalMorphTrigger>

Frequently asked questions

Is Morph Trigger Modal free for commercial use?
Yes. Morph Trigger Modal is MIT licensed, so you can use it in personal and commercial projects without attribution.
Why a native dialog element rather than a portal?
Because the hard parts come free with it. showModal() puts the dialog in the top layer so no ancestor's overflow can clip it, traps focus, handles Escape, makes the rest of the page inert, and returns focus when it closes. A hand-rolled version has to remember the previously focused element and restore it itself, and that is precisely the code that breaks when the trigger has unmounted in the meantime. close() hands focus back, or to the body if the trigger has gone, without throwing. It is also what the other modals in this library use, so two overlapping overlays behave as one system.
Is Morph Trigger Modal accessible for keyboard and screen reader users?
Yes. The trigger is a native button carrying aria-haspopup and aria-expanded, and the dialog is a real dialog element with aria-modal, labelled by its heading and described by its optional supporting line only when that line exists. Tab and Shift+Tab are trapped by the top layer rather than by a handler that has to enumerate focusable elements, Escape and a press on the backdrop both request a close, and focus returns to the trigger the moment closing begins rather than after the morph finishes. With reduced motion enabled the morph and the blur are dropped, and nothing about the trap, the scroll lock or the focus return changes.

Related components

Want the full page this belongs to?Template Empire builds complete Next.js templates with this motion system throughout. Browse the paid templates that pair with these free components.Explore Template Empire templates