A cross-platform UI framework written entirely in Rust.
One codebase → desktop, Android, and the web. GPU-rendered. Built from components. No DSL, no codegen, no bespoke CLI — just cargo.
Quick start · Gallery · Architecture · Status · Contributing · Français
The sample application moving between four of its screens. Every pixel — the layout, the type, the charts, the springs — is drawn by frus on the GPU.
frus is a greenfield attempt at a UI framework designed in Rust from day one: the entire framework — renderer, layout, widgets, gestures, theming, animation, accessibility — is Rust. There is no embedded VM, no second language for app logic, and no platform channel between your code and the pixels.
The parts that must be native (window creation, IME, screen readers, the Android activity) live behind one thin crate, frus-shell. Everything above it is portable.
use frus::{button, column, row, text, Align, BuildContext, FrusApp, Variant, Widget};
// A widget is what it builds. One that has something to remember keeps it with a hook.
fn counter(cx: &BuildContext) -> Box<dyn Widget> {
let count = cx.use_state(|| 0);
let (up, down) = (count.clone(), count.clone());
Box::new(column![
text(format!("{}", count.get())).size(48.0),
row![
button("+", move || up.update(|n| *n += 1)).variant(Variant::Filled),
button("−", move || down.update(|n| *n -= 1)).variant(Variant::Outlined),
].gap(20.0),
].gap(16.0).align(Align::Center))
}
// One declaration wires up desktop, Android and web entry points.
frus::main!(FrusApp::from_fn(counter));That is a complete, runnable application. cargo run on desktop, cargo apk run on Android, wasm-bindgen for the browser — the source does not change.
A widget with nothing to remember is a StatelessWidget; one with something to keep is a StatefulWidget whose State outlives the rebuilds — with init_state, did_update_widget, dispose and a set_state you can call from any handler. A plain function can keep state too, with use_state, use_ref, use_memo and use_effect.
use frus::{button, column, BuildContext, GoRoute, GoRouter, FrusApp, TextField, Widget};
fn sign_in(cx: &BuildContext) -> Box<dyn Widget> {
let email = cx.use_text_controller(""); // the field's text, held outside the field
let router = cx.router();
Box::new(column![
TextField::new("").label("Email").controller(&email),
button("Continue", move || router.go(format!("/welcome/{}", email.text()))),
])
}
let router = GoRouter::new(vec![
GoRoute::new("/", |_, _| frus::component(sign_in)),
GoRoute::new("/welcome/:name", |_, state| {
frus::text(format!("Hello, {}", state.param("name").unwrap_or("you")))
}),
]);
frus::main!(FrusApp::router(router));The router keeps a stack of pages, slides between them, answers the back gesture, follows redirects, and keeps a covered page's state until it is popped.
| One language, top to bottom | App logic, widgets, layout, and the renderer are all Rust. No FFI boundary in the hot path, no serialization across a bridge. |
| Components you can test without a window | A component is a value that says what it looks like, and its state is plain Rust beside it. Made, updated, disposed: the whole lifecycle runs in tests with no GPU and no window — as do more than 1,900 of this repo's tests. |
| GPU-native rendering | wgpu targets Vulkan, Metal, DX12, and WebGPU from one backend. Vector paths are tessellated with lyon; text is shaped by cosmic-text. |
| Everything is overridable | Widgets ship themed defaults, never hardcoded ones. If a widget paints it, you can restyle it or swap the slot. |
| cargo-native | No frus doctor, no custom package manager, no generated build directory. cargo build, cargo test, cargo apk run. |
All of this is one application — crates/frus-demo — and one source tree.
Every picture above except the phone is rendered, through the same pipeline a
window uses: cargo run -p frus-demo --features shots --bin shots -- docs/media.
They are regenerated after a change rather than slowly going stale.
Prerequisites: Rust 1.88 or newer — the minimum supported version, checked in CI — and a GPU with Vulkan, Metal, or DX12 drivers.
git clone https://github.com/KalybosPro/frus
cd frus
cargo run -p frus-hello # the counter above
cargo run -p frus-demo # a larger todo/kanban app
cargo run -p frus-transforms # animation and transform showcase
cargo test --workspace # ~2,180 tests; the rendering ones need a GPUThe repo ships a cargo generate template that produces a project wired for desktop and Android:
cargo install cargo-generate # once
cargo generate --path templates/app --name my-app
cd my-app && cargo runThe template asks for the path to your frus checkout — frus is not on crates.io yet, so dependencies resolve through path. See docs/getting-started.md.
cargo install cargo-apk # once
cargo apk run -p frus-demo --lib # build, install, launchRequires the Android SDK + NDK with ANDROID_HOME / ANDROID_NDK_ROOT set and a device visible to adb devices.
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli
cargo build -p frus-hello --target wasm32-unknown-unknown --profile web-release
wasm-bindgen --target web --no-typescript \
--out-dir crates/frus-hello/web/pkg \
target/wasm32-unknown-unknown/web-release/frus_hello.wasm
cd crates/frus-hello/web && python3 -m http.server 8080Needs a WebGPU-capable browser (Chrome/Edge 113+) on a secure context. Details in crates/frus-hello/web/README.md.
Four layers. Dependencies only ever point downward, and only frus-shell knows what platform it is on.
┌──────────────────────────────────────────────────────────────┐
│ Application what you write │
│ frus (facade) · frus-hello · frus-demo · frus-transforms │
├──────────────────────────────────────────────────────────────┤
│ Shell platform layer │
│ frus-shell — Application, Command, Subscription, │
│ lifecycle, IME, a11y, net, main! │
├──────────────────────────────────────────────────────────────┤
│ Widgets UI & interaction │
│ frus-widgets — Ui/scene, widget tree, gestures, theme │
├──────────────────────────────────────────────────────────────┤
│ Foundations render & measure │
│ frus-core · frus-layout · frus-text · frus-gpu · │
│ frus-image · frus-l10n │
└──────────────────────────────────────────────────────────────┘
| Crate | Role |
|---|---|
frus |
Facade — the single dependency an app needs. Re-exports shell + widgets + main!. |
frus-core |
Geometry, color (incl. HCT), paths, decorations, text styles, animation, scene graph, semantics. |
frus-layout |
Flexbox layout over taffy. |
frus-text |
Shaping and measurement via cosmic-text. |
frus-gpu |
wgpu device, 2D painter, path tessellation, glyph atlas, compositor, offscreen rendering. |
frus-image |
PNG/JPEG decoding to ImageData. |
frus-l10n |
i18n via Fluent bundles + locale negotiation. |
frus-widgets |
The widget library and interaction model (~150 modules). |
frus-shell |
Window, event loop, lifecycle, Command/Subscription, IME, AccessKit, fetch. |
frus-test |
Headless rendering, snapshots, golden-image comparison. |
frus-hello |
The canonical minimal app. Source of the cargo generate template. |
frus-demo |
Larger sample app exercising most widgets. |
frus-fetch-example |
End-to-end network example: RemoteData, loading/error/data states. |
frus-transforms |
Animated showcase of transforms, aspect ratio, fractional sizing. |
Read ARCHITECTURE.md before your first non-trivial change — it explains where a given kind of code belongs and why.
Pre-alpha. The core is real and exercised by three sample apps, but the API is not stable and nothing is published to crates.io yet.
| Platform | State | Notes |
|---|---|---|
| Desktop (Windows / Linux / macOS) | Working | winit + wgpu, clipboard, screen-reader a11y via AccessKit, dev live-reload |
| Android | Working | Native activity, Vulkan, real IME (composition & swipe), insets, system bars, the system clipboard, lifecycle — validated on device |
| Web (wasm + WebGPU) | Functional | Rendering, input, animation, subscriptions, async effects & fetch. Clipboard, a11y and live-reload are not wired up |
| iOS / macOS native | Not started | The shell layer is isolated, so adding a target is a contained job |
What works today: flex/grid/wrap layout, 1D & 2D scrolling with fill-then-scroll, text input with IME, drag-and-drop reordering with live reflow, data tables, editable grids, charts, date/time pickers, dropdowns, trees, toasts, modals, drawers, navigation with spring transitions and back-gesture, an overridable theme, RTL and i18n, spring, implicit and explicit animations, a wheel picker, lifecycle, effects and subscriptions, async HTTP with typed JSON, and golden-image testing (169 reference images).
Known gaps — these are the best places to help:
- Publishing to crates.io (everything is
path-based today). - Web clipboard, accessibility, and live-reload.
- iOS and native macOS shells.
- Text rendering edge cases, and broader golden coverage.
- A searchable documentation site built from the design notes.
See ROADMAP.md for the full picture.
The project is early enough that a single pull request can shape a subsystem. These are real, open, and written up with where to look and how to know you are done:
🟢 Turn on missing_docs, crate by crate |
Start with the small crates. One crate is a whole PR. |
| 🟡 Publish to crates.io | The single biggest thing between the project and anyone trying it. |
| 🟡 The overscroll stretch effect | Current Android stretches the content instead of glowing. A render-target effect, and where to start reading is written down. |
| 🟡 Clipboard and accessibility on the web | Both exist on desktop; the web drops them on the floor. |
| 🔴 An iOS shell | The architecture bets this is a contained job. Nobody has tested the bet. |
🟢 good first issue · 🟡 help wanted · 🔴 design first — all open issues
Not sure where you fit? Open an issue and say what you enjoy working on. English or French, both fine.
Start with CONTRIBUTING.md. The short version:
cargo test --workspace # must be green
cargo clippy --workspace --all-targets
cargo fmt --allEvery change ships with tests; every non-trivial change ships with a design note. Discussion happens in issues and discussions — English or French, both fine.
By participating you agree to the Code of Conduct.
- Getting started — write and run your first app
- Structuring an application — splitting a growing app across modules
- Architecture — how the crates fit together
- Roadmap — what's next and where help is wanted
- Design notes index — 501 notes, one per milestone: the analysis, the alternatives considered, the decision, and why. This is the project's real memory.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.





