leptos-modal
๐ Modal composable for Leptos โ minimal, type-safe, zero CSS to fight.
leptos-modal is the modal layer every app ends up writing by hand: you wrap the root once, define a modal as a plain component, and open it from anywhere โ no mounting it deep in the tree, no prop-drilling a setter down five levels, no z-index wars. The data you pass in is checked by the compiler, not by your users at runtime.
๐ค Features
Type-safe by construction โ pass the wrong data to a modal and
cargostops you, not the person clicking the button. Generics carry the input and context types end to end.Context and input, kept separate โ the constant stuff (like a user-delete callback) goes in once on registration; the dynamic stuff (which user) goes in on open. Two channels, no
Optionsoup.Close from wherever you are โ call the
close()handed to your modal, ormodal.close()from the controller. No setter threaded down the component tree.ARIA-compliant โ
role="dialog",aria-modal, the lot, out of the box.Esc closes it โ keyboard handling you didn't have to wire up.
A
#[modal]proc macro โ write a normal component, get a registerable modal.Zero external CSS dependencies โ nothing to import, nothing to override.
Portal-style rendering โ one place modals render, always exactly one on screen (why would you ever want to show two at once? ๐คจ), with a z-index that always wins.
Table of Contents
โธป
๐ฆ Installation
Add it to your Cargo.toml:
[dependencies]
leptos-modal = "0.3"Or from the terminal:
cargo add leptos-modalโธป
๐งช Usage
Set up the modal collector
Wrap your app with ModalCollector to enable modal rendering. Think of it like a provider in React โ it lets any of its descendants instantiate and use modals without each one having to mount them.
use leptos::prelude::*;
use leptos_modal::prelude::*;
#[component]
fn App() -> impl IntoView {
view! {
<ModalCollector>
<MainContent />
</ModalCollector>
}
}Create a modal component
Say your app has a user list, and you want to delete a user from it. A confirmation dialog would come in handy.
A leptos-modal modal is a component with this signature:
#[modal]
pub fn ConfirmationModal(input: Input, ctx: Context, close: fn()) -> impl IntoView;Where:
Inputis dynamic data you don't know until the modal opens (here, the user to delete). Must satisfyClone + Send + Sync + 'static.Contextis something constant, passed in once on registration and not changeable after (here, the function that does the deleting). Must satisfyClone + Copy + Send + Sync + 'static.closeis the handle that dismisses the modal โ call it from inside.
Here's 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>
}
}Open and close the modal
Register it with the use_modal! macro, then open it with the input:
#[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 (the context is the delete callback)
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>
}
}Closing happens from inside the modal via the injected close() (see the "Cancel" button above), from the controller with modal.close(), or simply by hitting Esc.
โธป
๐ API Reference
ModalCollector
Singleton component that manages modal state and rendering. We recommend it wraps your app root.
#[modal]
Proc macro that turns a normal component into one the ModalCollector can render. Your function keeps the (input, ctx, close) signature; the macro does the wiring.
use_modal!
Registers a modal and returns a typed controller:
// Without context
let modal = use_modal!(ModalComponent);
// With context
let modal = use_modal!(ModalComponent, context);The controller exposes:
open(input)โ opens the modal with the provided input.close()โ closes the modal.
Closing a modal
There's no single global close โ closing is always tied to a modal you've registered. You have three ways to dismiss one:
Inside the modal component, call the injected
close()(the third argument of the signature).From the controller returned by
use_modal!, callmodal.close().Let the user press
Escโ theModalCollectorhandles that for you.
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(())Built-in behaviour
Accessibility โ proper ARIA attributes (
role="dialog",aria-modal).Keyboard navigation โ
Esccloses the modal out of the box.Portal rendering โ modals render in one place, and there's always exactly one on screen at a time (why would you want more than one modal up at once? ๐คจ).
Overlay โ semi-transparent backdrop with proper positioning.
Responsive โ full viewport coverage with centered content.
โธป
๐ ๏ธ Requirements
Rust 2024 edition โ the crate targets the 2024 edition.
Leptos 0.8 โ built and tested against the 0.8 line.
โธป
๐ Repo & Contributions
๐ ๏ธ Repo: https://github.com/dsplce-co/leptos-modal ๐ฆ Crate: https://crates.io/crates/leptos-modal
Contributions, issues, ideas? Hit us up ๐ค
โธป
๐ License
MIT or Apache-2.0, at your option.