leptos-modal

Crates.ioleptos-modal
lib.rsleptos-modal
version0.3.1
created_at2025-11-26 21:22:27.637488+00
updated_at2026-01-13 13:54:02.547823+00
descriptionModal composable for Leptos
homepage
repository
max_upload_size
id1952323
size73,221
Dmndz (d14mndz)

documentation

README

We're dsplce.co, check out our work on our website: dsplce.co ๐Ÿ–ค

leptos-modal

๐ŸŒ€ Modal composable for Leptos โ€” A minimal and type-safe framework for modals in Leptos applications.


๐Ÿ–ค Features

โœ… Type-safe modal system with generics
โœ… Pass additional context to your modals
โœ… Close your modals from anywhere with a global fn
โœ… ARIA-compliant
โœ… Esc key handling
โœ… Portal-style rendering with proper z-index
โœ… Proc macro for modal component creation
โœ… Zero external CSS dependencies


๐Ÿ“ฆ Installation

Add to your Cargo.toml:

[dependencies]
leptos-modal = "0.3"

This crate requires Rust 2024 edition and is compatible with Leptos 0.8.


๐Ÿงช Usage

1. Set up modal collector

Wrap your app with ModalCollector to enable modal rendering. This works similar to a provider in React, in that it allows modals to be instantiated and used from the level of any of its descendants.

use leptos::prelude::*;
use leptos_modal::prelude::*;

#[component]
fn App() -> impl IntoView {
    view! {
        <ModalCollector>
            <MainContent />
        </ModalCollector>
    }
}

2. Create modal component

Imagine in your application there is a user list view, and you want to add the functionality to delete a user to it. You decide a confirmation dialog would come in handy.

In leptos-modal, your modal component needs to adhere to the following signature:

#[modal]
pub fn ConfirmationModal(input: Input, ctx: Context, close: fn()) -> impl IntoView;

Where:

  • Input is dynamic data typically not known until the modal's opening is triggered (in our example it would be the user to delete). Should satisfy Clone + Send + Sync + 'static
  • Context is something constant, passed to the modal on registration and thus not changeable (eg. a function responsible for user deletion). Should satisfy Clone + Copy + Send + Sync + 'static

Let's implement the confirmation dialog:

use leptos_modal::prelude::*;

#[modal]
pub fn ConfirmationModal(user: User, ctx: Callback<String>, close: fn()) -> impl IntoView {
    view! {
        <div class="confirmation-modal">
            <h2>"Confirm Action"</h2>
                <p>{move || format!("Are you sure you want to delete {}?", user.name)}</p>

            <div class="confirmation-modal__actions">
                <button on:click=move |_| close()>"Cancel"</button>
                <button on:click=move |_| {
                    ctx.run(user.id.clone());
                    close();
                }>"Confirm"</button>
            </div>
        </div>
    }
}

3. Use the modal

Now that you've defined the confirmation modal, let's call it using the use_modal! macro:

#[derive(Clone)]
struct User {
    id: String,
    name: String,
}

#[component]
fn UsersView(users: Vec<User>) -> impl IntoView {
    let delete_user = Callback::new(move |id: String| {
        // Deletion logic
    });

    // Registers the modal
    let modal = use_modal!(ConfirmationModal, delete_user);

    let on_delete = move |user: User| {
        modal.open(user);
    };

    view! {
        // โ— Notice the `ConfirmationModal` is not mounted directly
        // anywhere โ€” it is the `ModalCollector`'s job to render modals
        <ul>
            {
                move || users.iter().map(|user| view! {
                    <li>
                        {user.name.clone()}
                        <button
                            on:click={
                                let user = user.clone();

                                move |_| {
                                    on_delete(user.clone());
                                }
                            }
                        >"Delete"</button>
                    </li>
                }).collect::<Vec<_>>()
            }
        </ul>
    }
}

๐Ÿ“ API Reference

ModalCollector

Singleton component that manages modal state and rendering. We recommend that it wraps your app root.

#[modal]

Proc macro that helps the ModalCollector render your modals.

use_modal!

Creates a typed modal controller:

// Without context
let modal = use_modal!(ModalComponent);

// With context
let modal = use_modal!(ModalComponent, context);

Returns a modal struct with the methods:

  • open(args) - Opens the modal with provided arguments
  • close() - Closes the modal

close

Close the modal from any component (that is a descendant of ModalCollector) in your application:

leptos_modal::close();

Defining a modal

With both context and input:

#[modal]
pub fn ModalComponent(input: Input, ctx: Context, close: fn()) -> impl IntoView {
    view! {
        // ...
    }
}

let modal = use_modal!(ModalComponent, context);
modal.open(input)

Skipping context:

#[modal]
pub fn ModalComponent(input: Input, ctx: (), close: fn()) -> impl IntoView {
    view! {
        // ...
    }
}

let modal = use_modal!(ModalComponent);
modal.open(input)

Skipping input:

#[modal]
pub fn ModalComponent(input: (), ctx: Context, close: fn()) -> impl IntoView {
    view! {
        // ...
    }
}

let modal = use_modal!(ModalComponent, context);
modal.open(())

Modal Features

  • Accessibility: Proper ARIA attributes (role="dialog", aria-modal)
  • Keyboard Navigation: Esc key closes modal out of the box
  • Portal Rendering: Modals have a single place to render, and there is always one modal visible at a time (why would you want to show more than one modal at a time? ๐Ÿคจ)
  • Overlay: Semi-transparent backdrop with proper positioning
  • Responsive: Full viewport coverage with centered content

๐Ÿ“ Repo & Contributions

๐Ÿ“ฆ Crate: crates.io/crates/leptos-modal
๐Ÿ› ๏ธ Repo: github.com/dsplce-co/leptos-modal

Contributions, issues, ideas? Hit us up ๐Ÿ–ค


๐Ÿ”’ License

MIT or Apache-2.0, at your option.


Commit count: 0

cargo fmt