compose-rust guide

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.

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.

ConstraintConsequence
No webviewNo WKWebView, WebView2 or WebKitGTK, and no Tauri or wry.
No bundled JVMThe renderer is compiled ahead of time. A JVM appears only in the development shell for the renderer itself.
No hand-written JNI or cinteropEvery boundary shim is generated from the Rust schema, so the two sides cannot drift silently.
UI code lives in RustKotlin is the renderer's implementation, not a place where you write features.
Compose-quality text and IMENo 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:

PartStatusWhat it is
The boundaryWorksThe 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 protocolWorksFixed-layout mutation records, written into one batch buffer and read in place by the renderer. No serialisation format runs on the hot path.
The schemaWorksWidgets, 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 traitWorksThe 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 downloadWorksThe build script fetches the prebuilt renderer for your target, checks it and links it. See Getting started.
The authoring APIPlanned#[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

Planned, not yet in the crate

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.

rust, planned shape (does not compile against 0.0.x)
#[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.

PlatformStatusRenderer 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.

PlatformExecutablePhysical footprint
macOS (arm64)28.76 MB35.1 MB
Linux (x86-64)37.93 MBnot yet measured
Windowsnot yet measurednot 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