Components
Drawer New · experimental
shadcn's Drawer with goo. The edge trails, sloshes, and splashes as you drag and fling it.
Customize
GoopDrawer has the API of shadcn-vue's Drawer, which is built on vaul-vue. goopUI doesn't use vaul: it's a rebuild on reka-ui's Dialog (focus trapping, scroll locking, dismissal) with goopUI's own drag engine. The drag, snap points, springs, and goo all run in one frame, so the goo moves with the panel exactly. The root takes vaul's props, emits, and v-models, so you can swap the imports and keep your code.
Installation
Nuxt
pnpm add @goopui/nuxt
export default defineNuxtConfig({
modules: ['@goopui/nuxt'],
})
The module auto-imports every GoopDrawer* part, adds goop.css, and points Tailwind at the component classes. It needs Tailwind CSS v4 and the shadcn tokens (--background, --muted, --foreground). You don't install vaul-vue.
Vue
pnpm add @goopui/vue
@import "tailwindcss";
@import "@goopui/vue/goop.css";
Usage
<script setup lang="ts">
// Nuxt auto-imports these. In plain Vue, import them:
import {
GoopButton,
GoopDrawer,
GoopDrawerClose,
GoopDrawerContent,
GoopDrawerDescription,
GoopDrawerFooter,
GoopDrawerHeader,
GoopDrawerTitle,
GoopDrawerTrigger,
} from '@goopui/vue'
</script>
<template>
<GoopDrawer>
<GoopDrawerTrigger as-child>
<GoopButton variant="outline">Open drawer</GoopButton>
</GoopDrawerTrigger>
<GoopDrawerContent>
<GoopDrawerHeader>
<GoopDrawerTitle>Move goal</GoopDrawerTitle>
<GoopDrawerDescription>Set your daily activity goal.</GoopDrawerDescription>
</GoopDrawerHeader>
<GoopDrawerFooter>
<GoopButton>Submit</GoopButton>
<GoopDrawerClose as-child>
<GoopButton variant="outline">Cancel</GoopButton>
</GoopDrawerClose>
</GoopDrawerFooter>
</GoopDrawerContent>
</GoopDrawer>
</template>
The parts are GoopDrawer, GoopDrawerTrigger, GoopDrawerContent, GoopDrawerHeader, GoopDrawerFooter, GoopDrawerTitle, GoopDrawerDescription, GoopDrawerClose, GoopDrawerOverlay, and GoopDrawerPortal. The vaul props go on GoopDrawer. All the goo lives on GoopDrawerContent, which also renders the overlay and, on bottom drawers, the drag handle.
Behavior
- Drag. The panel follows your finger, with a rubber band past full open. Release, and it snaps open, to a snap point, or closed, with vaul's thresholds. Opens and snaps travel fast and land with a small overshoot. Closes fall away.
- Lag. The leading edge trails behind fast motion. A quick pull stretches it out behind the panel (
lag). - Slosh and rise. When the panel stops (release, a snap point, landing), the edge overshoots and wobbles (
slosh). Opening adds an extra jelly overshoot as it lands (rise). - Stretch. Dragging past full open pulls a globby bump toward your finger (
stretch). - Handle. The drag handle is a goo blob. It swells when you grab it, stretches along the drag, and trails a droplet (
handle). - Splash. Fling it away and a splash of drops tears off the leading edge, flies, falls, and fades (
splash). - Content stays sharp. Only the panel's background is goo, drawn on an
aria-hiddensurface behind your content. Like the sheet, it draws with the canvas renderer by default.
Directions
direction is vaul's. The goo edge is whichever side faces the middle of the screen:
<GoopDrawer direction="right">...</GoopDrawer>
Snap points
snapPoints takes viewport fractions (0 to 1) or px strings ('300px'), least visible first. Bind the current one with v-model:active-snap-point. The edge sloshes every time it lands on one. fadeFromIndex sets the snap point from which the overlay is fully shown.
<script setup lang="ts">
const snapPoints = [0.35, 0.65, 1]
const snap = ref<number | string | null>(snapPoints[0])
</script>
<template>
<GoopDrawer v-model:active-snap-point="snap" :snap-points="snapPoints">
<GoopDrawerTrigger as-child>
<GoopButton variant="outline">Snap points</GoopButton>
</GoopDrawerTrigger>
<GoopDrawerContent class="h-full max-h-[97%]">...</GoopDrawerContent>
</GoopDrawer>
</template>
Background scale
Like vaul, an open drawer scales the page behind it back, with rounded corners, and paints <body> black behind it. Mark the element that wraps your page:
<template>
<div data-vaul-drawer-wrapper class="min-h-dvh bg-background">
<NuxtPage />
</div>
</template>
The engine looks for [data-vaul-drawer-wrapper] first, then [data-goop-drawer-wrapper], so an app already set up for vaul works as it is. The scale follows the drag every frame, and the wrapper's own styles are put back when the drawer closes. Without a wrapper nothing is scaled. Turn it off with :should-scale-background="false", or keep the scale but skip the black body with :set-background-color-on-scale="false" (or no-body-styles). This site has no wrapper, so the demos above don't scale it.
Nested drawers
Open a GoopDrawer with nested inside another one and the parent recedes while the child is open, following the child as you drag it.
Events
GoopDrawer emits vaul's events: update:open, update:activeSnapPoint, drag (the share of the panel dragged), release (whether it stays open), close (it started closing), and animationEnd (an open or close finished).
Plain shadcn
:goo="false" gives exactly shadcn's DrawerContent look. It's still driven by goopUI's drag engine, so the vaul props keep working:
<GoopDrawerContent :goo="false">...</GoopDrawerContent>
Driving it from code
GoopDrawerContent exposes the live drawer controller as handle (null while closed). Open and close it with v-model:open on GoopDrawer.
Props
GoopDrawer
vaul-vue's DrawerRoot props, with the same names and defaults:
| Prop | Type | Default | Notes |
|---|---|---|---|
open | boolean | Controlled open state (v-model:open). | |
defaultOpen | boolean | Open by default when uncontrolled. Skips the first enter animation. | |
direction | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | The screen edge the drawer is attached to. |
snapPoints | (number | string)[] | Viewport fractions (0 to 1) or px strings ('300px'), least visible first. | |
activeSnapPoint | number | string | null | the first | The current snap point (v-model:activeSnapPoint). |
fadeFromIndex | number | the last | Snap index from which the overlay is fully shown. Below it the overlay stays clear. |
closeThreshold | number | 0.25 | Share of the panel (0 to 1) that has to be dragged off before a release closes it. |
shouldScaleBackground | boolean | true | Scale the page wrapper back while open. See Background scale. |
setBackgroundColorOnScale | boolean | true | With shouldScaleBackground: paint <body> black behind the scaled page. |
dismissible | boolean | true | false: dragging, clicking outside, and Escape don't close it. |
modal | boolean | true | false: the page stays interactive and outside clicks don't close it. |
nested | boolean | false | Opened on top of another GoopDrawer. The parent recedes while this one is open. |
handleOnly | boolean | false | Only the handle starts a drag. |
noBodyStyles | boolean | false | <body> gets no styles, so no black background on scale. |
scrollLockTimeout | number | 500 | Accepted for vaul compatibility. Scroll locking is reka-ui's. |
fixed | boolean | Accepted for vaul compatibility. No keyboard repositioning yet. | |
preventScrollRestoration | boolean | Accepted for vaul compatibility. |
GoopDrawerContent
Takes reka-ui's DialogContent props, class, and these goo props. The effect props take true, false, or a strength from 0 to 2:
| Prop | Type | Default | Notes |
|---|---|---|---|
goo | boolean | true | Master switch. false renders shadcn's DrawerContent look. The drag engine still drives it. |
lag | boolean | number | true | The leading edge trails behind fast motion. A fast pull stretches it out behind the panel. |
slosh | boolean | number | true | The edge overshoots and wobbles when motion stops: on release, at snap points, and on landing. |
rise | boolean | number | true | Opening lands with an extra jelly overshoot of the edge. |
stretch | boolean | number | true | Dragging past full open pulls a globby bump toward the finger. |
handle | boolean | number | true | The drag handle is a goo blob: it swells, stretches along the drag, and trails a droplet. |
splash | boolean | number | true | Fling-dismiss: a splash of drops tears off the leading edge, flies, falls, and fades. |
speed | number | 1 | Animation speed multiplier. |
preset | 'subtle' | 'playful' | 'wild' | app config, then playful | The overall feel. |
renderer | 'svg' | 'canvas' | app config, then 'canvas' | How the goo is drawn. A drawer is a big surface, so it uses canvas unless you or the app config say otherwise. |
The trigger, close, header, footer, title, description, overlay, and portal take shadcn-vue's props.
Accessibility
Focus trapping, scroll locking, Escape and outside-click dismissal, and ARIA attributes come from reka-ui. With dismissible off, Escape and outside clicks don't close it, so give it a close button. The goo surface and the handle are aria-hidden. Under reduced motion you can still drag it and it still snaps, but there's no goo motion: no lag, slosh, rise, stretch, or splash, and snaps land without the wobble. See Accessibility & reduced motion.