Skip to content

Replace sonner.js + lazy loader with a pure Rust toast system #29

@max-wells

Description

@max-wells

Context

The current Sonner integration relies on three JS/CSS files loaded globally:

File Size Role
public/components/sonner.js ~35KB Third-party Sonner library — renders toasts into [data-sonner-toaster]
public/components/sonner.min.js ~14KB Minified version
public/components/sonner.css ~8.6KB Sonner styles
public/components/lazy_load_sonner.js ~4KB Lazy loader — defers loading sonner.js until SonnerTrigger or SonnerList appears in DOM

The Leptos side (app_crates/registry/src/ui/sonner.rs) provides:

  • SonnerTrigger — a button with data-toast-title, data-toast-description, data-toast-position, data-variant attributes. Sonner.js reads these on click and renders a toast.
  • SonnerList / SonnerContainer / SonnerToaster — the DOM container where Sonner.js injects toast elements.

The JS library owns all toast state: creation, stacking, animation, auto-dismiss timers, and swipe-to-dismiss. Leptos has no visibility into this state.

What needs to happen

Replace the entire JS layer with a pure Rust/Leptos implementation.

A toast_custom module already exists at app_crates/registry/src/ui/toast_custom/ and appears to be an in-progress Rust toast system — use this as the foundation rather than starting from scratch. Audit what it already covers before building anything new.

Core requirements

1. Global toast signal

A RwSignal<Vec<ToastData>> (or similar) provided via Leptos context, accessible anywhere without prop drilling:

// Call from anywhere in the component tree
show_toast().success("Saved!");
show_toast().error("Something went wrong");
show_toast().with_description("Title", "Description");

2. SonnerTrigger → pure Rust click handler

Replace the data-* attribute pattern + JS click interception with a Leptos on:click:

// Before: JS reads data-toast-title on click
// After: Rust calls show_toast() directly
<button on:click=move |_| show_toast().push(toast)>
    {children()}
</button>

3. SonnerToaster → reactive Leptos renderer

SonnerToaster reads the signal and renders toasts as Leptos components — no JS injection into the DOM.

4. Auto-dismiss timers

Use set_timeout (from leptos::leptos_dom::helpers or gloo-timers) to auto-remove toasts after a configurable duration.

5. Animations

CSS-only enter/exit animations (no JS animation engine). Sonner's CSS can serve as reference; the goal is to replicate the visual feel without the library.

6. Variants

Preserve all existing ToastType variants: Default, Success, Error, Warning, Info, Loading.

7. Positions

Preserve all SonnerPosition variants: TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight.

Files to delete

  • public/components/sonner.js
  • public/components/sonner.min.js
  • public/components/sonner.css
  • public/components/lazy_load_sonner.js

Files to update

  • app/src/shell.rs — remove sonner CSS preload/load tags
  • app_crates/registry/src/ui/sonner.rs — rewrite using Leptos signals
  • app_crates/registry/src/demos/demo_sonner.rs / demo_sonner_variants.rs / demo_sonner_positions.rs
  • public/registry/styles/default/sonner.md — update registry snapshot

Testing

A comprehensive e2e test suite already exists at e2e/tests/components/sonner.spec.ts (355KB). Run it against the Rust implementation to verify behavioral parity before closing this issue. All existing scenarios must pass.

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