dioxus-compose guide

Getting started

From a clean checkout to a window on screen

Today this path is macOS only. The renderer is compiled ahead of time into a shared library, and your Rust binary links against it, so the renderer has to be built before any Rust example will run.

Prerequisites

Before anything else, run ./scripts/setup-check.sh. It checks every tool the build and the quality gate need, names what is missing, and prints the command that fixes it. Everything in the table below is on its list.

WhatWhy
macOS on Apple silicon or IntelThe native build script exits immediately on anything but Darwin.
Xcode command line toolsThe build compiles a few C and Objective-C files and links the library.
A Rust toolchain (stable)The Host crate. rustup is the usual way in.
Liberica NIK 25 FullBuilds the renderer with native-image. See the note below, an ordinary GraalVM will not do.
Use Liberica NIK, not upstream GraalVM

Upstream GraalVM skips AWT support on Darwin (oracle/graal#13272, still open as of 2026-09). Compose Desktop draws through AWT, so an image built with upstream GraalVM has no window to show. Liberica NIK Full links AWT statically, which is why it is the required toolchain on macOS.

The build script looks for NIK in the standard install location and otherwise reads GRAALVM_HOME:

shell
# Auto-detected if installed here:
~/Library/Java/JavaVirtualMachines/bellsoft-liberica-vm-full-openjdk25*/Contents/Home

# Otherwise point at it explicitly:
export GRAALVM_HOME=/path/to/bellsoft-liberica-vm-full-openjdk25/Contents/Home

If you have no NIK at all, ./scripts/install-nik.sh installs it. The install is addressed by version, so a warm cache makes a second run a no-op, the download happens once per NIK version, not once per invocation. The build script itself refuses an installation without lib/static/darwin-*/libawt_lwawt.a, which catches a stock GraalVM up front instead of at the end of a long build.

Kotlin itself needs no separate install. The renderer project ships a self-bootstrapping ./kotlin wrapper that downloads the pinned Kotlin Toolchain on first use.

Build the renderer

One script does the whole thing: it runs the renderer briefly on the NIK JVM to capture the exact runtime classpath, compiles the C entry points, and then builds the shared library.

shell
./dioxus-compose-renderer/desktop/scripts/build-native.sh

The result is a self-contained runtime directory:

dioxus-compose-renderer/build/native-image/dist/lib/
libdioxus_compose_renderer.dylib   # the renderer: AWT, Skiko JNI, Compose, our code
libskiko-macos-<arch>.dylib        # Skia, which Skiko loads by path
libjawt.dylib                      # forwards JAWT_GetAWT into the renderer
libawt_lwawt.dylib                 # placeholder that libawt loads by path

The three small libraries alongside the renderer are not padding. Statically linked AWT on macOS still looks for some things by file path at runtime, and these fill those slots. Architecture explains the detail; the short version is that the whole deployment layout is one lib/ directory whose parent the renderer reports as java.home.

If you keep a renderer somewhere else, a downloaded artifact, a vendored copy, point DIOXUS_COMPOSE_RENDERER_DIR at its lib directory and the Rust build will use that instead of the workspace one (NFR-10).

The first build is slow, native-image compiles Compose, Skiko and AWT ahead of time. You only repeat it when the Kotlin side changes; normal Rust work reuses the library.

Smoke-test the library

Before involving Rust, check that the library alone can open a window. This links a tiny C host against it and calls dioxus_compose_renderer_run:

shell
./dioxus-compose-renderer/desktop/scripts/smoke-test.sh

A window should appear. Closing it makes run return 0 and the process exit normally. That is the acceptance criterion for the macOS runtime requirement (SPEC PR-8), and it passed on 2026-09-20.

Run the demo

The desktop example is behind the native-renderer feature, because that feature is what makes the Rust build link against the library you just produced:

shell
cargo run -p dioxus-compose --features native-renderer --example desktop_demo

Type a message and press Enter to send it, or Shift+Enter for a newline. If you use a Korean, Japanese or Chinese IME, press Enter while a syllable is still composing: it should commit the composition and not send the message. That distinction is the point of the whole design and is covered in Writing UI.

Without the feature flag

Building without native-renderer gives you a Host with a no-op renderer: launch returns immediately and no window appears. That build is what the unit tests and benchmarks use, so it is useful, just not something you can look at.

Run the checks

The repository's quality gates are one script: formatting, Clippy with warnings denied, the test suite, the benchmarks in Criterion's quick mode, and the Kotlin build and tests.

shell
./scripts/check.sh              # quick benchmark sampling, for presubmit
./scripts/check.sh --full       # full benchmark sampling
./scripts/check.sh --no-kotlin  # Rust only; DXC_SKIP_KOTLIN=1 does the same

One of those tests compares the checked-in Kotlin protocol file against what the Rust schema generates. If you change the schema and forget to regenerate, the test fails, which is the intended behaviour, not an inconvenience.