Building a Desktop and Web App with Dioxus
This page answers one task: you want a single Rust codebase for an application’s user interface that ships as a web app (compiled to WebAssembly) and as a desktop app (Windows, macOS, Linux), without maintaining two UIs. Dioxus is designed for this; you want to know how a project is set up, how platform-specific code is separated, and what each target costs.
Prerequisites
- [ ] Rust with the
wasm32-unknown-unknowntarget installed. - [ ] The Dioxus CLI (
cargo install dioxus-cli, which providesdx). - [ ] Platform prerequisites for desktop builds (a system WebView on each OS).
How Dioxus targets several platforms
Dioxus is a React-like UI library for Rust: components are functions returning markup written with the rsx! macro, and state lives in signals that
trigger re-renders when they change. The same component tree is rendered by different renderers. The web renderer compiles the app to WebAssembly and
updates the browser DOM. The desktop renderer compiles the app natively and drives a system WebView (WebView2 on Windows, WKWebView on macOS, WebKitGTK on
Linux), sending DOM edits from native Rust code to the WebView. Because the desktop app runs natively, it has full access to the file system, threads and
native libraries, while the web app runs inside the browser’s sandbox.
That split determines how you structure code: UI components and application logic are shared; anything that touches the platform — files, networking specifics, OS integration — sits behind small interfaces with one implementation per target.
Step 1 — create the project
dx new notes-app # choose a template; select web and desktop platforms when asked
cd notes-app
dx serve --platform web # dev server with hot reload in the browser
dx serve --platform desktop # native window with the same UI
The generated Cargo.toml declares features for each platform (for example web and desktop) that enable the matching Dioxus renderer; dx selects the
feature for the platform you build. Keep this structure: platform choice through features, not through separate crates for the UI.
Step 2 — write shared components with signals
use dioxus::prelude::*;
#[component]
fn App() -> Element {
let mut notes = use_signal(Vec::<String>::new);
let mut draft = use_signal(String::new);
rsx! {
h1 { "Notes" }
input { value: "{draft}", oninput: move |e| draft.set(e.value()) }
button {
onclick: move |_| { notes.write().push(draft()); draft.set(String::new()); },
"Add"
}
ul { for n in notes.iter() { li { "{n}" } } }
}
}
fn main() { dioxus::launch(App); }
This code runs unchanged on both targets. Event handlers are Rust closures; state updates re-render the affected parts of the tree.
Step 3 — isolate platform-specific code
Persistence differs: the web app stores notes in IndexedDB or OPFS (through web-sys or a crate built on it); the desktop app writes files. Define a trait
and implement it per platform behind cfg:
pub trait Store { fn load(&self) -> Vec<String>; fn save(&self, notes: &[String]); }
#[cfg(feature = "web")]
pub fn store() -> impl Store { web_store::LocalStorageStore::new("notes") }
#[cfg(feature = "desktop")]
pub fn store() -> impl Store { desktop_store::FileStore::new(dirs::data_dir().unwrap().join("notes.json")) }
Keep platform modules small and test the shared logic natively with cargo test. Platform crates that do not compile for one target belong only under that
target’s feature, so they never enter the other build — the same principle as in
using Cargo features for Wasm and native builds.
Step 4 — handle assets and styling
Use Dioxus’s asset system (asset!("/assets/style.css")) so images and stylesheets are bundled correctly for both targets — copied into the web build’s
output with hashed names, and embedded or packaged with the desktop app. Styling is regular CSS in both cases, since both render HTML: the desktop app’s
WebView applies the same stylesheet the browser does. Test layout in each OS’s WebView, which may differ slightly from the browser engine used for the web
build.
Step 5 — build and measure both targets
dx bundle --platform web --release # static files: index.html, .wasm, JS glue, assets
dx bundle --platform desktop --release # installer/app bundle for the current OS
Check the web build’s Wasm size — a Dioxus app with a few dependencies is commonly several hundred kilobytes compressed — and apply the usual size
techniques (opt-level = "z", wasm-opt, removing panic formatting). The desktop app’s size is dominated by the native binary and, on platforms without a
system WebView, the WebView runtime; on Windows, WebView2 is usually present on current systems.
Server functions and full-stack mode
Dioxus also supports full-stack apps: server functions written in Rust and called from components, with the server rendering initial HTML and the client hydrating. That adds a third target (the server binary) to the same codebase. For a desktop-and-web app, server functions let both clients call the same backend with type-checked requests. Start without them if the app is local-first; add them when a backend appears.
Testing
Test shared logic natively — fast and simple. Test components with Dioxus’s testing utilities or by rendering to a string and asserting output. Run end-to-end tests of the web build with Playwright, and smoke-test the desktop build on each OS in CI (launch, open a window, perform one action), since WebView differences and packaging problems show up only there.
Async work and long-running tasks
Both targets need to keep the UI responsive, but they offer different tools. In the web build, async tasks run on the browser’s event loop through
wasm-bindgen-futures, and CPU-heavy work must be chunked or moved to a Web Worker; there are no OS threads. In the desktop build, Dioxus runs on a native
async runtime, and heavy work can move to a background thread with std::thread or tokio::task::spawn_blocking. Write shared code against Dioxus’s
spawn and use_resource hooks for async work, which map to the right executor on each platform, and keep CPU-heavy functions pure so the platform layer
can decide where to run them — a worker on the web, a thread on the desktop. A function that takes input and returns output without touching UI state is
easy to move; one that mutates signals throughout its run is not.
Distribution and updates
The web app updates the moment you deploy; the desktop app needs installers and an update mechanism. dx bundle produces platform packages (MSI or NSIS on
Windows, DMG or app bundles on macOS, AppImage or deb on Linux), and you will need code signing for Windows and notarisation for macOS to avoid security
warnings. Plan an update channel — a self-update library, the platform’s store, or a simple “new version available” check — from the first release, because
users of an unsigned, non-updating desktop app tend to stay on old versions indefinitely. Keep the app’s data format versioned so newer web and older desktop
clients can coexist if they share a backend.
Expected output
dx serve --platform web and dx serve --platform desktop show the same notes UI; notes persist in IndexedDB on the web and in a JSON file on the desktop;
shared logic tests run with cargo test; the web bundle’s Wasm is 410 KB compressed after size optimisation; and desktop bundles build for Windows, macOS
and Linux in CI.
Gotchas
- Platform crates in shared code. They break the other target. Gate them behind features.
- Assuming identical rendering. OS WebViews differ from browsers in details. Test each.
- Unoptimised web builds. Large downloads. Apply Wasm size settings.
- Blocking work in event handlers. Freezes both targets’ UI. Use async tasks.
- Separate UI crates per platform. Duplication creeps back. Share components.
- No update mechanism for desktop builds. Users stay on old versions. Plan signing and updates from the first release.
Performance note
The notes app’s web build shrank from 1.2 MB to 410 KB compressed with opt-level = "z", LTO, wasm-opt -Oz and panic-formatting removal; the desktop
build started in about 250 ms on a laptop.
Frequently Asked Questions
Does the desktop app run Wasm? No — it compiles natively; only the web build uses WebAssembly.
Can I target mobile too? Dioxus has mobile renderers; treat them as additional platforms with the same structure.
How do I call JavaScript libraries on the web?
Through wasm-bindgen imports in the web-only module, or Dioxus’s eval facility.
Is hot reloading available on desktop?
dx serve supports hot reloading of markup on both platforms.
Where should CPU-heavy work run in each build? In a Web Worker for the web build and on a background thread for the desktop build; keep the function pure so the platform layer can choose.
Do desktop builds need code signing? In practice yes — unsigned apps trigger security warnings on Windows and macOS; plan signing and notarisation.
Related
- Comparing Yew, Leptos and Dioxus — choosing a framework.
- Sharing one Wasm core across web and desktop — a core-only alternative.
- Using Wasm in a Tauri app — another desktop route.
- Shrinking Rust Wasm with Cargo profiles — web size.
← Back to Full-Stack Frameworks with Wasm