Native UI, written in Rust
Compose-shaped UI in Rust, drawn by Compose.
compose-rust gives Rust a Compose-shaped API: composable functions, remember
and recomposition. An ahead-of-time compiled Compose renderer draws what they describe. No
webview is embedded, and no JVM ships with your application.
What it is
compose-rust is four things put together.
- A Compose-shaped Rust API with a slot table and a recomposition
runtime. A composable is a function, and state lives in
remember. When state changes, only the scopes that read it run again. Nothing compares trees to find what changed. Planned for 1.0.0. - A narrow C ABI boundary and a schema. Rust defines the widgets, properties and records once. The Kotlin types and the platform shims are generated from that definition, never written by hand. Works
- An AOT-compiled Compose renderer. It is compiled ahead of time to native code on every platform, to Kotlin/Wasm in the browser, and to an Android library on Android. Compose's own text layout, widgets and platform IME do the drawing and the typing. Works
- No webview, and no bundled JVM. On the desktop the goal is one executable that links only the system's own libraries. 0.0.1 does not reach it yet; see Platforms. Partial
Why it exists
It is aimed at applications that stay open all day, where weight is felt every hour. A
webview brings a browser engine with it. A bundled Java runtime adds 80 to 120 MB, even
after jlink.
A pure-Rust UI toolkit fails where text-heavy applications need it most: text itself. Korean, Japanese and Chinese input goes through the platform IME, and an IME that is 90% right makes an application unusable. Compose Multiplatform already has mature text, IME and widget behaviour. compose-rust borrows exactly that. Rust keeps the application code, and Compose interprets the UI tree that Rust sends it.
| Constraint | Consequence |
|---|---|
| No webview | No WKWebView, WebView2 or WebKitGTK, and no Tauri or wry. |
| No bundled JVM | The renderer is compiled ahead of time. A JVM appears only in the development shell for the renderer itself. |
| No hand-written JNI or cinterop | Every boundary shim is generated from the Rust schema, so the two sides cannot drift silently. |
| UI code lives in Rust | Kotlin is the renderer's implementation, not a place where you write features. |
| Compose-quality text and IME | No code path bypasses Compose's platform text input. |
Status
0.0.x is the name reservation and renderer distribution stage. The API will change. This is what the 0.0.1 crate contains today:
| Part | Status | What it is |
|---|---|---|
| The boundary | Works | The compose_rust_host_* C functions the renderer calls, Host, LaunchBuilder, launch_runtime, and request_frame_from_worker for waking a frame from a worker thread. |
| The protocol | Works | Fixed-layout mutation records, written into one batch buffer and read in place by the renderer. No serialisation format runs on the hot path. |
| The schema | Works | Widgets, properties, modifiers, design roles, Theme and DesignSystem. A schema hash stops a Host and a renderer built from different schemas from running together. The codegen binary generates the Kotlin side from it. |
The Runtime trait | Works | The interface that whatever builds the tree implements, plus the types around it: assets, custom drawing, input events, notifications, messages, palettes and window size classes. |
| The renderer download | Works | The build script fetches the prebuilt renderer for your target, checks it and links it. See Getting started. |
| The authoring API | Planned | #[composable], remember, the Recomposer, launch and the widget functions. They arrive with 1.0.0 on 2026-10-20, tracked in #23 and #64. |
Until 1.0.0 the crate is a foundation for a layer that builds the tree. It is not yet something you write screens with directly.
The planned shape
The code below shows the intended shape of the authoring API. It does not compile against 0.0.x, and the names are not final.
#[composable]
fn counter() {
let count = remember(|| mutable_state_of(0));
Column(Modifier::new().fill_max_width(), || {
Text(format!("{}", count.get()));
Button(|| count.set(count.get() + 1), || Text("+1"));
});
}
fn main() {
compose_rust::launch(counter);
}
Three things about it are settled. #[composable] inserts the groups, so you never
write groups or keys by hand, and they close correctly through branches, loops, early
return, ? and unwinding. Names follow Compose. The widget vocabulary
is the one in the schema, so nothing is defined twice. Writing UI
describes the rest of the planned API.
Prefer Dioxus and rsx!? Use dioxus-compose, which runs on top of compose-rust and supports both HTML/CSS rsx! and Compose-widget rsx!.
Platforms
The table lists the renderer each platform gets in 0.0.1. "Works end to end" means the platform has been used by hand. "Builds and starts" means CI builds it and a smoke test opens and draws a window, which proves rather less.
| Platform | Status | Renderer in 0.0.1 |
|---|---|---|
| macOS (arm64) | Works end to end | A GraalVM native-image library beside the application. The Kotlin/Native renderer linked into the executable (its own window, drawing through Metal) runs in CI and replaces it in a following release (#22). Basic Korean IME input works; the full IME checklist is not finished. |
| Linux (x64, arm64) | Builds and starts | A GraalVM native-image library. The Kotlin/Native renderer (its own X11 window, drawing through GLX) passes a headless startup test for both architectures in CI and replaces it in a following release. |
| Windows | Builds and starts | A GraalVM native image, smoke-tested on every renderer change. Moving to Kotlin/Native so that Windows is one executable too (#24). |
| iOS | Builds and starts | A Kotlin/Native static archive exporting the same C symbols, released as an XCFramework. |
| Android | Works end to end | A Kotlin Activity owns the process and the loop, and Rust is a cdylib. The JNI shims on both sides are generated from the schema. The crate carries the renderer's Kotlin sources, and its build script stages them into the Gradle project. |
| Web (wasm) | Works end to end | The Kotlin/Wasm module owns one WebAssembly.Memory and the Rust module imports it, so a batch is read where it was written. Calls from the renderer to the Host cross a generated JavaScript forwarder, measured at about 12 ns. Direct calls without the forwarder are planned (#54). |
What it weighs
These numbers are for the notepad sample, built for release and stripped, with the Kotlin/Native renderer linked in. That is the path that becomes the default after 0.0.1, not what 0.0.1 ships. The result is one executable, with no runtime beside it and no virtual machine inside it.
| Platform | Executable | Physical footprint |
|---|---|---|
| macOS (arm64) | 28.76 MB | 35.1 MB |
| Linux (x86-64) | 37.93 MB | not yet measured |
| Windows | not yet measured | not yet measured |
For scale, the same kind of application on macOS was four files and 93.1 MB, with a 56 MB footprint, while the renderer still carried a Java runtime. In the browser, the renderer and Skia together are 6.95 MB gzipped.
Where to go next
Getting started
Add the crate, let the build fetch the renderer, and build this repository.
Writing UI
The planned authoring API: composables, state, effects, events and animation.
Widget reference
Every widget, modifier and role in the schema, and where each widget keeps its state.
Lists & streaming
Windowed lists, streamed text, and work on worker threads.
Design systems
Seven design systems behind one set of roles, unified or adaptive.
Architecture
The boundary, the records, the measure call, zoom and the renderer per platform.