# leptosbook Guide
A complete tour of building gesture-driven, paginated interfaces with Leptos `0.9`.
## Contents
1. [Mental model](#mental-model)
2. [Folio](#folio)
3. [Built-in navigation](#built-in-navigation)
4. [Driving navigation yourself](#driving-navigation-yourself)
5. [Gestures](#gestures)
6. [The install prompt](#the-install-prompt)
7. [Styling](#styling)
8. [Recipes](#recipes)
9. [Gotchas](#gotchas)
---
## Mental model
leptosbook is built around one component: **`Folio`**. Think of it as a book.
- You hand it a `Signal<Vec<T>>` and a `render` function.
- It shows exactly **one item at a time**.
- It owns all navigation state and shares it through a **context** (`FolioContext`).
- Any descendant — a nav bar, a tab strip, a progress dot — can read that context (`use_folio_context()`) to display state or drive a turn.
```
swipe / key / click
│
▼
Folio updates current_page ──► FolioContext signals fire
│ │
▼ ▼
page re-renders with your descendants
a direction-aware re-render (counters,
slide animation tabs, dots, …)
```
That's the whole architecture. Everything below is detail.
---
## Folio
```rust
use leptos::prelude::*;
use leptosbook::prelude::*;
#[derive(Clone)]
struct Item { title: String, body: String }
#[component]
fn Reader(items: Signal<Vec<Item>>) -> impl IntoView {
view! {
<Folio
items=items
render=|it: Item| view! {
<article>
<h1>{it.title}</h1>
<p>{it.body}</p>
</article>
}
>
<FolioNav/>
</Folio>
}
}
```
### Props
| `items` | `Signal<Vec<T>>` | — | The pages. Changing it reactively re-renders. |
| `render` | `impl Fn(T) -> IV + Clone + Send + Sync + 'static` | — | Called **only for the visible page**. |
| `threshold` | `f64` | `60.0` | Min pointer travel (px) to count as a swipe. |
| `inject_css` | `bool` | `true` | Inject `FOLIO_CSS`. Set `false` to fully self-style. |
| `on_page_change` | `Option<Arc<dyn Fn(usize) + Send + Sync>>` | `None` | Fires after each turn with the new 0-based index. |
| `empty_fallback` | `Option<ChildrenFn>` | `None` | Rendered when `items` is empty. |
| `children` | `Option<Children>` | `None` | Rendered *below* the page slot. |
`T: Clone + Send + Sync + 'static`.
### Notes that bite people
- **`items` is a `Signal`, not a `Vec`.** For a static list, wrap it: `Signal::derive(|| vec![…])`.
- **`render` runs once per visible page**, not once per item. Keep it cheap; it re-runs on every turn.
- **`children` render below the page**, inside the same focusable `.folio` container — that's why `<FolioNav/>` as a child can read the context.
- Only **horizontal** intent pages by trackpad wheel; vertical scroll is left alone (`touch-action: pan-y`).
---
## Built-in navigation
### FolioNav
Prev button, `current / total` counter, next button. Auto-disables at the ends. Must be inside a `<Folio>`.
```rust
<FolioNav/> // ← / →
<FolioNav prev_label="Back".to_string()
next_label="Next".to_string()/>
```
| `prev_label` | `String` | `"←"` |
| `next_label` | `String` | `"→"` |
### FolioTabs
A tab strip. It is **decoupled from context on purpose** — you supply `active` and `on_select`, so it works with a Folio *or* with any other indexed state.
| `tabs` | `Vec<&'static str>` |
| `active` | `ReadSignal<usize>` |
| `on_select` | `Arc<dyn Fn(usize) + Send + Sync>` |
To bind it to a Folio, pull `current_page` and `go_to` out of the context:
```rust
#[component]
fn BookTabs() -> impl IntoView {
let ctx = use_folio_context();
let go_to = ctx.go_to.clone();
view! {
<FolioTabs
tabs=vec!["Overview", "Details", "Pricing"]
active=ctx.current_page
on_select=std::sync::Arc::new(move |i| go_to(i))
/>
}
}
```
---
## Driving navigation yourself
`use_folio_context()` returns a `FolioContext`. Call it from any descendant of `<Folio>` (it panics otherwise — that panic is your signal that the component isn't nested correctly).
```rust
#[derive(Clone)]
pub struct FolioContext {
pub current_page: ReadSignal<usize>,
pub total_pages: Signal<usize>,
pub go_next: Arc<dyn Fn() + Send + Sync + 'static>,
pub go_prev: Arc<dyn Fn() + Send + Sync + 'static>,
pub go_to: Arc<dyn Fn(usize) + Send + Sync + 'static>,
pub anim_epoch: ReadSignal<u64>,
pub last_dir: ReadSignal<Option<TurnDir>>,
}
pub enum TurnDir { Forward, Backward }
```
**You navigate by calling closures**, and **read state from signals**:
```rust
let ctx = use_folio_context();
// clone the closures you need into handlers
let go_next = ctx.go_next.clone();
let go_prev = ctx.go_prev.clone();
let at_start = move || ctx.current_page.get() == 0;
let at_end = move || ctx.current_page.get() + 1 >= ctx.total_pages.get();
view! {
<button on:click=move |_| go_prev() disabled=at_start>"Prev"</button>
<span>{move || format!("{} / {}", ctx.current_page.get() + 1, ctx.total_pages.get())}</span>
<button on:click=move |_| go_next() disabled=at_end>"Next"</button>
}
```
### Reacting to turns
`anim_epoch` ticks on every turn; `last_dir` tells you which way. Use either to trigger side effects or custom animations:
```rust
let ctx = use_folio_context();
### `SwipeConfig`
```rust
use leptosbook::SwipeConfig;
let cfg = SwipeConfig::default() // threshold 60, keyboard + mouse on
.threshold(100.0)
.no_keyboard()
.no_mouse();
```
It's a builder for **your own** handlers. It is **not yet** consumed by `<Folio>` — Folio currently exposes only the `threshold` prop and always listens for touch/mouse/wheel/keyboard. Wiring `SwipeConfig` into Folio is on the roadmap.
---
## The install prompt
```rust
<InstallPrompt/>
```
- On **iOS Safari**: shows "Add to Home Screen" instructions (since iOS has no programmatic install).
- On **Chrome/Edge/Android**: captures `beforeinstallprompt` and shows an "Install" button that triggers the native prompt.
- Stays silent if already running standalone, or if previously dismissed (remembered via `localStorage["pwa_dismissed"]`).
| `title` | `String` | `"Install this app"` |
| `description` | `String` | `"Add it to your home screen for the best experience."` |
| `inject_css` | `bool` | `true` |
```rust
<InstallPrompt
title="Install Acme".to_string()
description="Get the full-screen, offline-ready experience.".to_string()
/>
```
`title` and `description` apply to the Chrome/Android prompt. On iOS the prompt
shows the standard "Add to Home Screen" share-sheet instructions instead.
---
## Styling
`FOLIO_CSS` is injected by default. To take over completely:
```rust
<Folio inject_css=false items=items render=render>
<style>{leptosbook::FOLIO_CSS}</style> // start from the defaults…
// …then your overrides, or omit FOLIO_CSS entirely for a clean slate
<FolioNav/>
</Folio>
```
### Class reference
| `.folio` | Outer focusable container |
| `.folio-page-slot` | Clipping viewport for the page |
| `.folio-page` | The current page (absolutely positioned) |
| `.folio-enter-left` / `.folio-enter-right` | Slide-in animation by direction |
| `.folio-empty` | Centering wrapper for `empty_fallback` |
| `.folio-nav` | `FolioNav` container |
| `.folio-nav-btn` / `:disabled` | Nav buttons |
| `.folio-nav-counter` | The `n / total` text |
| `.folio-tabs` | `FolioTabs` container |
| `.folio-tab` / `.folio-tab.active` | Individual tab |
| `.install-prompt`, `.install-prompt-*` | From `INSTALL_CSS` |
The animation direction is chosen from `last_dir`: forward turns slide `folio-enter-left`, backward turns slide `folio-enter-right`.
---
## Recipes
### Static list
```rust
```rust
#[component]
fn Dots() -> impl IntoView {
let ctx = use_folio_context();
view! {
<div class="dots">
{move || (0..ctx.total_pages.get()).map(|i| {
let cur = ctx.current_page.get();
view! { <span class:on=move || i == cur>"•"</span> }
}).collect_view()}
</div>
}
}
```
---
## Gotchas
- **`use_folio_context()` outside a `<Folio>` panics.** Keep nav components as descendants.
- **`items` must be a `Signal`.** A bare `Vec` won't type-check.
- **`render` must be `Clone + Send + Sync`.** Avoid capturing non-`Send` state; clone what you need in first.
- **`FolioTabs` won't follow the page on its own** — wire `active`/`on_select` to the context as shown above.
- **`SwipeConfig` doesn't affect `<Folio>` yet.** Use the `threshold` prop for now.
---
For runnable code, see [`examples/`](examples/). For task-focused walkthroughs, see the [cookbook](https://andygauge.github.io/leptosbook).