Spring Dialog
A free React modal animation built on the native dialog element, with a spring scale-in and blurred backdrop.
- Automated axe check
- Reduced-motion tested
- SSR build verified
- Motion
- 2 runtime packages
Spring Dialog is a free react modal animation for Next.js and React, built with Motion and Tailwind on top of the native dialog element. Opening the modal fades in a blurred backdrop while the panel springs from 0.94 scale and a slight vertical offset to rest, and closing plays a quick 0.18 second fade. Because it uses showModal, the focus trap, top layer rendering and Escape handling come from the browser rather than from JavaScript re-implementations.
The component is fully controlled through open and onClose props, locks background scroll while open, and never lets the exit animation delay focus: the moment the dialog closes, focus returns to the button that opened it. The visible title labels the dialog for screen readers, the close button is a real button with an accessible name, a click on the backdrop closes the modal, and with prefers-reduced-motion enabled both directions become plain opacity fades of 150 milliseconds or less with no scale or movement.
Publish with confidence
Destructive and important actions deserve a beat of friction. Open the dialog to see the spring entrance and focus handling.
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-springUsage
"use client";
import { useState } from "react";
import { ModalSpring } from "@/components/modal-spring";
export default function Page() {
const [open, setOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open dialog
</button>
<ModalSpring
open={open}
onClose={() => setOpen(false)}
title="Publish changes?"
>
<p>Publishing makes your updated sections live for every visitor.</p>
</ModalSpring>
</>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | required | Controlled open state. |
| onClose | () => void | required | Called when the dialog asks to close: Escape, a click on the backdrop or the close button. |
| title | string | required | Visible h2 heading that names the dialog via aria-labelledby. |
| children | ReactNode | required | Dialog body content. |
| className | string | undefined | Additional classes merged onto the panel. |
Customisation
Spring Dialog declares no tokens of its own; the panel and backdrop consume the global semantic tokens through Tailwind utilities, and the spring itself is the shared springSnappy preset from lib/motion.ts, so retuning that preset retunes every springing component together. The className prop is merged onto the panel, which is the usual place to change its width or padding:
:root {
--border: oklch(from var(--color-neutral-50) l c h / 0.16);
}
<ModalSpring
open={open}
onClose={() => setOpen(false)}
title="Publish changes?"
className="max-w-lg p-8"
>
...
</ModalSpring>Frequently asked questions
- Is Spring Dialog free for commercial use?
- Yes. Spring Dialog is MIT licensed, so you can use it in personal and commercial projects without attribution.
- Is Spring Dialog accessible for keyboard and screen reader users?
- Yes. It renders a native dialog element opened with showModal, so the browser provides the focus trap, top layer rendering and Escape handling, and the page behind it becomes inert. The visible title labels the dialog through aria-labelledby, the close button is a real button named Close dialog, background scroll is locked while the modal is open, and when it closes focus returns to the button that opened it immediately, never waiting for the exit animation.
- How does the spring animation work, and what happens with reduced motion?
- Motion animates only transform and opacity: the backdrop fades in with a blur over 300 milliseconds while the panel springs from 0.94 scale and an 8 pixel offset to rest using the shared springSnappy preset, and closing is a 0.18 second fade handled by AnimatePresence. With prefers-reduced-motion enabled the scale and movement are dropped entirely and both opening and closing become plain opacity fades of 150 milliseconds or less.