# A drawer that follows the finger · Components

<!-- https://learn-ui.com/chapters/components/drawer -->

Emil Kowalski’s second library is **Vaul** (Vaul is a drawer component for React, built by Emil Kowalski to recreate the feel of Apple's iOS Sheet on the web.), “A drawer component for React.” [Building a drawer component](https://emilkowal.ski/ui/building-a-drawer-component) opens with the motive: “Using Apple’s Sheet component on iOS feels natural, I wanted to create the same experience, but for the web.” He prefers a drawer to a modal on mobile. Vercel’s existing one lacked drag-to-dismiss and had input problems, so he built a new one and open-sourced it for the feedback.

### Borrowed anatomy, written feel

Vaul sits on Radix’s Dialog primitive, which “ensures the component is accessible, handles focus management etc.” The API mirrors Radix: Drawer.Root, Trigger, Portal, Content, Overlay, plus Handle, Title, Description, and Close on the [API page](https://vaul.emilkowal.ski/api).

What Emil wrote is the feel, starting with one line of CSS. The curve “closely matches the one used in iOS; it’s from the Ionic Framework,” and 500 ms “is also supposed to mimic iOS’s Sheet.” His thesis: “Using the right easing and duration makes a big difference in terms of how this, and any other component, feels.”

Note that 500 ms exceeds the under-300 ms rule he gives elsewhere; a drawer is large and imitates a system component with its own timing.

> Figure. CSSEmil Kowalski, Building a drawer component

Source

The curve, the duration, and the Ionic attribution are in the Motion section of the post. The under-300 ms rule is from You Don’t Need Animations and 7 Practical Animation Tips, covered in [the Motion chapter](https://learn-ui.com/chapters/motion/practical-tips).

### Dragging without dropping frames

“Not losing frames while dragging is a good start, but I already failed there.” Past roughly 20 list items, dragging lagged with no re-renders to blame. Each update wrote a CSS variable feeding translateY, and CSS variables are inheritable, so every child recalculated style. Writing the transform directly on the element fixed it, a quick fix in retrospect that took hours.

> Figure. TypeScriptEmil Kowalski, Building a drawer component

Then momentum, so a flick closes the drawer, and damping at the top, where it moves less the further you pull: “things in real life don’t suddenly stop, they slow down first.” A shouldDrag function refuses the drag unless content is scrolled to the top, as on iOS, and since fast scrolling overshoots, a 100 ms timeout after reaching the top forbids dragging.

Touches after the first are ignored until release so a second finger does not make the drawer jump. “They are ‘invisible’ because they align with users’ inherent expectations.”

### Reaching outside the sheet

scaleBackground makes the body look like another sheet: Vaul finds \[vaul-drawer-wrapper\] and applies a transform and border radius that follow drag progress, so 40% down sets the radius to 60% of its maximum. **Snap points** (Checkpoints in a drawer you can drag to and it will snap to; Emil Kowalski describes them as a common pattern on iOS and points to Apple Maps.) are a fraction of the viewport or fixed pixels, snapped to the closest on release and momentum-based; fixed values keep an input sticking out evenly on every device. Apple Maps is the model.

Inputs get the Visual Viewport API: Vaul listens for visualViewport resize and sets the drawer’s height and bottom from the new height, so it sits above the keyboard and stays scrollable. The cost is a slight delay, since the event fires once the keyboard is fully visible.

One idea did not ship: dimming Safari’s theme-color bar with the overlay. The meta tag cannot transition or take semi-transparent colors, so he computed the opaque equivalent, built 50 interpolated colors, and updated the tag every 10 ms to match the 500 ms transition, eased with the same bezier via bezier-easing. It stays out of Vaul because it drifts when frames drop.

His last point is testing: a real phone on a cable with Safari’s devtools, or the Xcode Simulator, never a shrunken desktop window. The README now calls the repo unmaintained; the decisions stand. Vaul is a short list of numbers chosen to match a reference.
