A panel that slides in from the edge of the screen and follows the finger. A drop-in replacement for Vaul, with the same API.
Installation
Install the packages
Add the shadcn/ui component
Animate it
The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.
shadcn/ui's Radix Drawer is built on Vaul, which is no longer maintained. Animate UI's Drawer has exactly Vaul's API (the Drawer object and its parts, the same props, the same data-vaul-* attributes), built on Radix UI's Dialog and Motion. animate-ui animate drawer swaps the vaul import for it, so you can also remove the vaul dependency from your project.
Usage
Import the primitive from Animate UI instead of Vaul. Everything else stays the same.
import { Drawer } from '@animate-ui/react/primitives/radix-ui/drawer';
export function MoveGoal() {
return (
<Drawer.Root>
<Drawer.Trigger>Move goal</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay />
<Drawer.Content>
<Drawer.Handle />
<Drawer.Title>Move goal</Drawer.Title>
<Drawer.Description>Set your daily activity goal.</Drawer.Description>
<Drawer.Close>Save</Drawer.Close>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}The import * as Vaul from 'vaul' style works too, as do the named exports (Root, NestedRoot, Content, Overlay, Portal, Handle) and the types (DialogProps, ContentProps, HandleProps, WithFadeFromProps, WithoutFadeFromProps). direction, snapPoints with activeSnapPoint, setActiveSnapPoint and fadeFromIndex, dismissible, handleOnly, modal, closeThreshold, shouldScaleBackground (on the element with data-vaul-drawer-wrapper), NestedRoot, container, onDrag, onRelease, onClose and onAnimationEnd behave as in Vaul, and data-vaul-no-drag keeps an element from starting a drag.
Animation
Animate UI v1 had no drawer: it keeps Vaul's API and behaviour, with Animate UI's motion.
- Content slides in from its edge (
direction, its own size plus any gap to the edge), opaque, a sheet sliding in rather than a fade, in 0.4s ofcubic-bezier(0.34, 1.2, 0.64, 1): it answers at once, goes a hair past its rest (about 1% of its size, which Vaul's bleed below it covers), then settles back. A hint of bounce, where Vaul's own curve zips in, then stops. With snap points, it slides to the active one the same way. - Closed (Escape, a click outside, a Close), it leaves quickly, seen speeding off its edge (0.16s on an ease-in,
cubic-bezier(0.4, 0, 1, 1)), so it never reads as a constant slide and its last pixels don't crawl out. - Its shadow, if your classes give it one, lifts off the page with it: none off screen, all of it once it's in (from its first snap point up), on every frame of a slide or a drag, so it fades as it leaves rather than riding out at full strength.
- Overlay comes in and clears exactly like a dialog's: its tint deepens while its backdrop blur grows from 0, in 400ms on a soft ease-out both ways, so the page behind blurs progressively (fading its opacity would cross-fade the sharp page with an already blurred one). Once a drag has moved the drawer, it also follows the drawer's position, like Vaul's; with snap points, it always does, coming in between the snap point before
fadeFromIndexand that one. Over a background image (a gradient), it fades its opacity instead. - Dragging follows the pointer on the same frame: the pointer sets the position directly, with no transition, no fade and no React render. Pulled past the open position, it rubber-bands. On release, it closes past
closeThreshold(25% of its size) or on a flick, and otherwise eases back on its curve. Dismissed by a drag, it's thrown: it keeps the finger's speed to its edge (linear, over the time the rest of the way takes at that speed, up to 360ms, like Base UI's drawer), so it neither stalls on release nor crawls out. With snap points, it goes to the closest one, a flick moves one point (a fast one skips to the first or last), and a flick down from the first closes it. - Scrollable content keeps scrolling: a drag doesn't start when the content under the pointer can scroll that way, nor shortly after it scrolled (
scrollLockTimeout). - A drag dismissal continues from where the pointer let go; opening again while it closes picks up from where it is. The click that ends a drag doesn't activate the button under it.
- Nested drawers: the parent steps back (16px towards its open edge, slightly smaller) on the drawer's curve while a nested drawer is open, and follows the nested drawer's drag.
shouldScaleBackgroundscales the[data-vaul-drawer-wrapper]element with the drawer's position, and paints the body black (setBackgroundColorOnScale, not withnoBodyStyles).forceMountdoesn't keep the closed drawer in the DOM: it plays its exit, then leaves, so a closed drawer never locks the page's scroll or hides it from screen readers.- Under reduced motion, the drawer fades in place and settles without moving; a drag still follows the pointer.
AnimateUIProvider temposcales every duration and spring.
Not reproduced from Vaul: its iOS Safari workarounds (position: fixed on the body, so preventScrollRestoration and disablePreventScroll are accepted but have no effect; Radix's Dialog locks the scroll of a modal drawer). repositionInputs and fixed keep a bottom drawer above the on-screen keyboard, more simply than Vaul. A Handle tap on a drawer without snap points does nothing.
API Reference
Every part takes exactly the props of Vaul's Drawer (its Content, Overlay, Trigger, Close, Title and Description take Radix UI's Dialog props). Animate UI adds the animation props below.
Root, NestedRoot, Trigger, Portal, Handle, Title, Description, Close
Vaul props only.
Content
Vaul props, plus:
| Prop | Type | Default |
|---|---|---|
transition? | Transition | { duration: 0.4, ease: [0.34, 1.2, 0.64, 1] } |
exitTransition? | Transition | { duration: 0.16, ease: [0.4, 0, 1, 1] } |
Overlay
Vaul props, plus:
| Prop | Type | Default |
|---|---|---|
transition? | Transition | { duration: 0.4, ease: [0.215, 0.61, 0.355, 1] } |
exitTransition? | Transition | transition |