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-unknown target installed.
  • [ ] The Dioxus CLI (cargo install dioxus-cli, which provides dx).
  • [ ] 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.

One Dioxus codebase, two targets Shared components and application logic sit on top. Below, a thin platform layer provides storage, file access and networking per target. The web target compiles to Wasm and renders into the browser DOM. The desktop target compiles natively and drives a system WebView. components + logic rsx!, signals, shared crates platform layer storage, files, OS features web renderer Wasm → browser DOM desktop renderer native → system WebView build + bundle dx serve / dx bundle

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.

Web and desktop builds of the same Dioxus app The web build compiles to Wasm, runs in the browser sandbox, needs no install and is sized by the Wasm download. The desktop build compiles natively, uses the system WebView for rendering, has full OS access including files and threads, and ships as an installer. web (Wasm) browser sandbox no install, instant updates size = Wasm download reach desktop (native + WebView) full file system, threads installer per OS native speed for logic capability

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.

Web bundle size after optimisation steps Compressed kilobytes of the Dioxus notes app's Wasm for the default release build, with size-oriented profile settings, and with wasm-opt and panic formatting removal added. KB compressed default release 1,200 KB opt-level z + LTO 640 KB + wasm-opt -Oz, no panic fmt 410 KB

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.

← Back to Full-Stack Frameworks with Wasm