Dialog with motion: it grows out of the button that opened it, shrinks back into it as it closes, glides to a new height when its content changes, and swipes down to close on a phone.
$ pnpm dlx shadcn@latest add https://dashral.com/r/motion-dialog.jsonInstallation
$ pnpm dlx shadcn@latest add https://dashral.com/r/motion-dialog.jsonAdds components/dashral/motion/dialog.tsx, the Dialog it builds on (in components/dashral/), motion and the motion files every motion component shares. With the registry in your components.json:
$ pnpm dlx shadcn@latest add @dashral/motion-dialogUsage
The same as Dialog, imported from components/dashral/motion/: swap the import and it moves.
import { Dialog, DialogClose } from "@/components/dashral/motion/dialog"
<Dialog
trigger={<Button>Edit profile</Button>}
title="Edit profile"
footer={<DialogClose asChild><Button>Done</Button></DialogClose>}
>
<ProfileForm />
</Dialog>Examples
Delete from a list
Each dialog grows out of its row's button. Delete shows Deleting…, the dialog shrinks back, the row folds away, and Undo brings it back.
Swipe down to close
On a phone, or in your browser's device mode: swipe the panel down and it follows your finger as the backdrop clears, slides away closed far or fast enough, and springs back short of it.
Compared with Default
Fades and zooms in from 95% in the middle of the screen. A new step's height jumps.
Motion
Grows out of the button that opened it on a spring and shrinks back into it; glides to a new height when what's inside changes; on a phone, swipe it down to close.
How the two editions differ in general, and which to use where: Motion.
API reference
Every prop of Dialog, and every attribute of the element it renders. What the Motion edition adds is marked Motion.
Dialog
Also takes every prop of Radix Dialog(opens in a new tab).
| Prop | Type | Default |
|---|---|---|
swipeToClose | boolean
| true |
trigger | ReactElementWhat opens it. Leave it out to open it with open / onOpenChange. | — |
title | ReactNodeRequired: it names the dialog. Hide it with classNames.title "sr-only" if the design has none. | — |
description | ReactNodeUnder the title; it describes the dialog. | — |
children | ReactNodeThe body. | — |
footer | ReactNodeButtons at the bottom. | — |
form | ComponentProps<"form">Makes the body and footer one form, e.g. { onSubmit } or { action }. | — |
size | "sm" | "md" | "lg" | "xl"The window's width. | "md" |
showCloseButton | booleanThe × in the corner. | true |
open / defaultOpen / onOpenChange | Controlled or initial state. | — |
classNames | { header, title, description, footer }Classes for each part. | — |
className | stringThe window's classes. | — |
DialogClose
Closes the dialog: wrap a Cancel button in the footer.
Also takes every prop of Radix Dialog(opens in a new tab).
| Prop | Type | Default |
|---|---|---|
asChild | booleanRender the child element instead (e.g. a Next.js Link), passing these styles and props to it. | false |
DialogClose is the default edition's, re-exported, so a footer works the same with either.
Motion
- It grows from 60% on a quick spring (--duration-standard, a hint of overshoot), from the middle of the button that opened it.
- Closing, it shrinks back toward that button and fades (--duration-fast, --ease-exit) while the backdrop clears; the backdrop's blur stays put and only fades, so it never costs frames.
- When what's inside changes (a form's next step, an error appearing) the panel glides to its new height with no overshoot; it's clipped only while it glides.
- On a touch screen, a swipe down on the panel (not in a field) moves it with your finger while the backdrop clears; past 110px, or flicked, it slides off the bottom closed and the phone buzzes; short of it it springs back. Upward it barely gives. A small handle at the top says so.
- Opened without a trigger (open and onOpenChange alone), it grows from the middle. With reduced motion it only fades, and doesn't swipe.