Skip to content

Replace vaul_drawer.js + CSS with a pure Rust implementation using web_sys #30

@max-wells

Description

@max-wells

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions