leptos-arrow-grid
High-performance, virtualised data grid for Leptos, powered by Apache Arrow.
Features
- Zero-copy rendering from
Arc<RecordBatch>— no serialisation overhead - Virtualised rows — renders only visible DOM nodes, handles millions of rows
- Excel-like selection — single click, Ctrl+click, Shift+click, drag
- 3-state column sort — Asc → Desc → Natural (per-column indicators)
- Column resize — drag header borders, min-width enforced
- Per-column filtering — Contains, StartsWith, Regex modes via kebab menu
- Context menu — right-click for Copy / Select All / Download CSV
- Clipboard TSV copy — Ctrl+C copies selected rows as tab-separated values
- CSV download — Ctrl+S or context menu downloads selection as a timestamped
.csv - Keyboard navigation — ↑↓ navigate, Shift+↑↓ extend, Ctrl+A select all, Ctrl+C copy, Ctrl+S download, Esc clear
- CSS-variable theming — bring your own design tokens (Catppuccin Mocha defaults)
- WASM-safe — no panics in production code, graceful fallbacks
The Data Contract
The grid does NOT fetch or cache data. It owns zero rows.
leptos-arrow-grid is a pure display component. You own all the data logic:
- You keep
page_start: RwSignal<u64>and update it fromon_viewport_change. - You keep
sort: RwSignal<SortState>and update it fromon_sort_change. - You derive a
Signal<Option<GridPage>>containing anArc<RecordBatch>for the current window. - You pass
total_rowsso the grid can size its scrollbar thumb correctly.
The grid tells you what the user wants; you decide how to satisfy it (in-memory sort, SQL query, async fetch, etc.).
Quick Start
Prerequisites
Cargo.toml
[]
= "0.1"
Link the stylesheet in your HTML (Trunk):
Minimal working example
use Arc;
use ;
use ;
use *;
use ;
See examples/playground/ for a full 1 M-row demo with in-memory sort and filtering.
Visual Playground
examples/playground/ is a self-contained Leptos app that exercises every feature of the grid.
It generates a synthetic in-memory dataset — no server, no database, no network requests.
What the playground demos
| Feature | How to try it |
|---|---|
| Dataset scale | 1 K / 100 K / 1 M rows toolbar buttons — scrollbar thumb shrinks as total grows |
| Virtual scrolling | Scroll freely; only ~100 DOM rows are ever alive |
| Column sort | Click a column header → Asc → Desc → Natural (3rd click removes sort) |
| Per-column filter | Click the ⋮ kebab on any header → type in the filter box; try Contains, StartsWith, or Regex |
| Combined sort + filter | Active at the same time; visible row count updates in the toolbar status |
| Row selection | Click → single row; Ctrl+click → add/remove; Shift+click → extend range; drag → lasso |
| Keyboard navigation | ↑ / ↓ move focus; Shift+↑↓ extend selection; Ctrl+A select all; Esc clear |
| Copy to clipboard | Select rows → Ctrl+C — pastes as TSV into any spreadsheet (requires localhost or HTTPS) |
| CSV download | Ctrl+S or right-click → Download CSV — or use the Save CSV toolbar button |
| Column resize | Drag the border between two column headers |
| Context menu | Right-click anywhere in the grid body |
| Selection counter | Bottom status bar shows live N rows selected count |
Prerequisites
# Rust WASM target (one-time)
# Trunk web bundler (one-time)
wasm-bindgen pin: The playground pins
wasm-bindgen = "=0.2.117"in itsCargo.toml. This is intentional — it avoids an upstreamexternrefbug that causes a blank screen in some browser/toolchain combinations. Do not remove the=prefix when bumping.
Run
Open http://localhost:8080 in your browser.
Trunk watches src/, the library source, and the stylesheet for changes and rebuilds
automatically. A first cold build takes ~10 s; incremental rebuilds after a source edit are
typically under 1 s.
To produce an optimised release bundle (useful for profiling render performance):
# artefacts land in examples/playground/dist/
Known Limitations (v0.1)
| Limitation | Notes |
|---|---|
| Fixed row height only | Height is set via the row_height prop (default 28 px). Dynamic heights are not supported. |
| Single-column sort | Multi-column sort is planned for a future release. |
| No column reordering or hiding | Drag-to-reorder is not implemented. |
| CSR only | No SSR support; #[cfg(target_arch = "wasm32")] guards all DOM code. |
| Clipboard requires HTTPS | Ctrl+C copy uses the browser Clipboard API which only works in a secure context (HTTPS or localhost). The on_copy_error callback fires otherwise. |
| Regex filter is substring match | The playground demo does not ship a regex engine; Regex mode falls back to substring matching. |
CSS Custom Properties
| Variable | Default | Description |
|---|---|---|
--lag-font-mono |
monospace |
Font family |
--lag-font-size-base |
13px |
Base font size |
--lag-font-size-small |
11px |
Small text (headers, row nums) |
--lag-bg-primary |
#1e1e2e |
Primary background |
--lag-bg-secondary |
#181825 |
Secondary background (row nums, menus) |
--lag-bg-surface |
#313244 |
Surface background (header, hover) |
--lag-text-primary |
#cdd6f4 |
Primary text colour |
--lag-text-secondary |
#a6adc8 |
Secondary text colour |
--lag-text-muted |
#6c7086 |
Muted text (row numbers, kebab) |
--lag-border |
#45475a |
Border colour |
--lag-accent |
#89b4fa |
Accent (selection, sort indicators) |
--lag-warning |
#f9e2af |
Warning (sort building) |
--lag-error |
#f38ba8 |
Error colour |
--lag-transition-fast |
100ms ease |
Hover/focus transition |
--lag-grid-header-height |
32px |
Header row height |
--lag-grid-cell-padding |
4px 8px |
Data cell padding |
See docs/theming.md for a complete light-mode override example.
Further Reading
| Document | Topic |
|---|---|
| docs/arrow-data-integration.md | Building RecordBatch from real data; using GridPage |
| docs/state-management.md | Why SelectionState is pure; wrapping in RwSignal |
| docs/theming.md | CSS variables in context; light-mode override snippet |
| TROUBLESHOOTING.md | Clipboard HTTPS, "?" cells, empty grid |
MSRV
Rust 1.94 (edition 2024). Requires wasm32-unknown-unknown target for browser use.
License
MIT OR Apache-2.0