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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions