Context
public/components/vaul_drawer.js (~474 lines) and public/components/vaul_drawer.css (~6.3KB) power the entire Drawer interaction layer. They are loaded as <script type="module"> and <link rel="stylesheet"> directly inside the Drawer component.
The Leptos side (app_crates/registry/src/ui/drawer.rs) only renders DOM structure — all open/close state, animations, drag physics, scroll detection, focus management, and keyboard handling live entirely in JS with no Leptos signal involvement.
What vaul_drawer.js does
State management (per drawer instance):
isOpen, isDragging, startPos, currentPos, drawerSize, dragStartTime, previousActiveElement
Open sequence:
- Removes
hidden from overlay and DrawerContent
- Optionally locks body scroll with scrollbar-width padding compensation
- Applies scale + translate transform to
[data-vaul-drawer-wrapper] (depth effect on the whole app)
- Sets
data-state="open" on overlay and drawer (triggers CSS transitions)
- Focuses first focusable element inside drawer (or the drawer itself)
Close sequence:
- Computes translate distance from element dimensions
- Applies
transform + opacity transitions to drawer and overlay
- Restores wrapper scale to
scale(1)
- After
500ms (transition duration): resets all inline styles, re-adds hidden, restores focus to previousActiveElement
Drag-to-dismiss (dismissible drawers only):
pointerdown — captures pointer, records start position and drawer size
pointermove — applies translate3d matching drag delta; in closing direction: also scales overlay opacity and wrapper scale proportionally; in opening direction: applies logarithmic damping (dampenValue)
pointerup — computes velocity (delta / timeTaken); closes if velocity > 0.4 OR delta / drawerSize >= 0.25, otherwise snaps back
shouldDrag() — prevents drag when scrollable child content is not at scroll top (Vaul's core scroll-conflict detection)
fixDrawerPosition() — corrects drawer's top when page has vertical scroll (needed because data-vaul-drawer-wrapper lives at app root)
Keyboard:
Escape → close
Tab / Shift+Tab → focus trapping within focusable drawer elements
Positions: Bottom (default), Left, Right — horizontal vs vertical axis detection throughout
Variants: Inset (default), Floating — overlay opacity behavior differs
The right Rust pattern
Since DrawerTrigger and DrawerClose already exist as Leptos components, state should move into a DrawerContext on the Drawer component (similar to SidenavContext), with a RwSignal<bool> for open/close. This lets DrawerTrigger and DrawerClose call ctx.open() / ctx.close() directly.
The complex parts (drag, pointer capture, viewport position fix, wrapper scale) require web_sys:
// Pointer capture — web_sys
element.set_pointer_capture(event.pointer_id())?;
element.release_pointer_capture(event.pointer_id())?;
// RAF for open animation sequencing
request_animation_frame(Closure::once(move || { ... }));
// Timeout for post-close cleanup
set_timeout(move || { ... }, Duration::from_millis(500));
// Scroll conflict detection
element.scroll_height() > element.client_height() && element.scroll_top() != 0
For CSS transitions, the existing vaul_drawer.css can be kept initially and replaced with Tailwind arbitrary values / inline styles as a follow-up — don't let CSS be a blocker.
Positions and variants to preserve
| Prop |
Values |
position |
Bottom (default), Left, Right |
variant |
Inset (default), Floating |
dismissible |
true (default), false |
lock_body_scroll |
true (default), false |
show_overlay |
true (default), false |
Files to delete / update
Delete:
public/components/vaul_drawer.js
public/components/vaul_drawer.css (or keep temporarily — see note above)
Update:
app_crates/registry/src/ui/drawer.rs — add DrawerContext, rewrite Drawer with Rust event handling, remove <script> and <link> tags
public/registry/styles/default/drawer.md — update registry snapshot
Testing
E2e test suite: e2e/tests/components/drawer.spec.ts (76KB). All existing scenarios must pass against the Rust implementation before closing this issue — in particular:
- Open / close via trigger and close button
- Drag-to-dismiss (velocity threshold and distance threshold)
- Snap-back when drag doesn't meet threshold
- Scroll conflict: drag blocked when scrollable child content is not at top
- Escape key closes drawer
- Tab / Shift+Tab focus trapping
dismissible=false disables overlay click and drag
- All three positions (
Bottom, Left, Right)
Floating variant overlay behavior
lock_body_scroll=false skips body scroll lock
Reference
Context
public/components/vaul_drawer.js(~474 lines) andpublic/components/vaul_drawer.css(~6.3KB) power the entire Drawer interaction layer. They are loaded as<script type="module">and<link rel="stylesheet">directly inside theDrawercomponent.The Leptos side (
app_crates/registry/src/ui/drawer.rs) only renders DOM structure — all open/close state, animations, drag physics, scroll detection, focus management, and keyboard handling live entirely in JS with no Leptos signal involvement.What vaul_drawer.js does
State management (per drawer instance):
isOpen,isDragging,startPos,currentPos,drawerSize,dragStartTime,previousActiveElementOpen sequence:
hiddenfrom overlay andDrawerContent[data-vaul-drawer-wrapper](depth effect on the whole app)data-state="open"on overlay and drawer (triggers CSS transitions)Close sequence:
transform+opacitytransitions to drawer and overlayscale(1)500ms(transition duration): resets all inline styles, re-addshidden, restores focus topreviousActiveElementDrag-to-dismiss (dismissible drawers only):
pointerdown— captures pointer, records start position and drawer sizepointermove— appliestranslate3dmatching drag delta; in closing direction: also scales overlay opacity and wrapper scale proportionally; in opening direction: applies logarithmic damping (dampenValue)pointerup— computes velocity (delta / timeTaken); closes ifvelocity > 0.4ORdelta / drawerSize >= 0.25, otherwise snaps backshouldDrag()— prevents drag when scrollable child content is not at scroll top (Vaul's core scroll-conflict detection)fixDrawerPosition()— corrects drawer'stopwhen page has vertical scroll (needed becausedata-vaul-drawer-wrapperlives at app root)Keyboard:
Escape→ closeTab/Shift+Tab→ focus trapping within focusable drawer elementsPositions:
Bottom(default),Left,Right— horizontal vs vertical axis detection throughoutVariants:
Inset(default),Floating— overlay opacity behavior differsThe right Rust pattern
Since
DrawerTriggerandDrawerClosealready exist as Leptos components, state should move into aDrawerContexton theDrawercomponent (similar toSidenavContext), with aRwSignal<bool>for open/close. This letsDrawerTriggerandDrawerClosecallctx.open()/ctx.close()directly.The complex parts (drag, pointer capture, viewport position fix, wrapper scale) require
web_sys:For CSS transitions, the existing
vaul_drawer.csscan be kept initially and replaced with Tailwind arbitrary values / inline styles as a follow-up — don't let CSS be a blocker.Positions and variants to preserve
positionBottom(default),Left,RightvariantInset(default),Floatingdismissibletrue(default),falselock_body_scrolltrue(default),falseshow_overlaytrue(default),falseFiles to delete / update
Delete:
public/components/vaul_drawer.jspublic/components/vaul_drawer.css(or keep temporarily — see note above)Update:
app_crates/registry/src/ui/drawer.rs— addDrawerContext, rewriteDrawerwith Rust event handling, remove<script>and<link>tagspublic/registry/styles/default/drawer.md— update registry snapshotTesting
E2e test suite:
e2e/tests/components/drawer.spec.ts(76KB). All existing scenarios must pass against the Rust implementation before closing this issue — in particular:dismissible=falsedisables overlay click and dragBottom,Left,Right)Floatingvariant overlay behaviorlock_body_scroll=falseskips body scroll lockReference
window_event_listener+ signal (keyboard shortcut pattern)