Drawer

Components

Drawer New · experimental

shadcn's Drawer with goo. The edge trails, sloshes, and splashes as you drag and fling it.

Drag it, flick it away, or pull it past full open.

Customize

Direction
Preset
Speed 1.00×
Lag 1.00×
Slosh 1.00×

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
nuxt.config.ts
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
main.css
@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-hidden surface 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:

app.vue
<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:

GoopDrawer props
PropTypeDefaultNotes
openbooleanControlled open state (v-model:open).
defaultOpenbooleanOpen 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.
activeSnapPointnumber | string | nullthe firstThe current snap point (v-model:activeSnapPoint).
fadeFromIndexnumberthe lastSnap index from which the overlay is fully shown. Below it the overlay stays clear.
closeThresholdnumber0.25Share of the panel (0 to 1) that has to be dragged off before a release closes it.
shouldScaleBackgroundbooleantrueScale the page wrapper back while open. See Background scale.
setBackgroundColorOnScalebooleantrueWith shouldScaleBackground: paint <body> black behind the scaled page.
dismissiblebooleantruefalse: dragging, clicking outside, and Escape don't close it.
modalbooleantruefalse: the page stays interactive and outside clicks don't close it.
nestedbooleanfalseOpened on top of another GoopDrawer. The parent recedes while this one is open.
handleOnlybooleanfalseOnly the handle starts a drag.
noBodyStylesbooleanfalse<body> gets no styles, so no black background on scale.
scrollLockTimeoutnumber500Accepted for vaul compatibility. Scroll locking is reka-ui's.
fixedbooleanAccepted for vaul compatibility. No keyboard repositioning yet.
preventScrollRestorationbooleanAccepted 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:

GoopDrawerContent goo props
PropTypeDefaultNotes
goobooleantrueMaster switch. false renders shadcn's DrawerContent look. The drag engine still drives it.
lagboolean | numbertrueThe leading edge trails behind fast motion. A fast pull stretches it out behind the panel.
sloshboolean | numbertrueThe edge overshoots and wobbles when motion stops: on release, at snap points, and on landing.
riseboolean | numbertrueOpening lands with an extra jelly overshoot of the edge.
stretchboolean | numbertrueDragging past full open pulls a globby bump toward the finger.
handleboolean | numbertrueThe drag handle is a goo blob: it swells, stretches along the drag, and trails a droplet.
splashboolean | numbertrueFling-dismiss: a splash of drops tears off the leading edge, flies, falls, and fades.
speednumber1Animation speed multiplier.
preset'subtle' | 'playful' | 'wild'app config, then playfulThe 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.