dioxus-compose guide

Overview

A Dioxus renderer for HTML and CSS, drawn by Compose

dioxus-compose runs a Dioxus app written the way one is written for the web, with div, h1, flexbox and a stylesheet, and draws it natively with Compose Multiplatform through compose-rust. There is no webview and no JVM in what you ship.

Where it stands

The HTML path lays a screen out and produces a drawing plan today, and it does not draw on screen yet. The bridge that hands that plan to the renderer is not built (partial, #43). Every claim on these pages is checked by tests on the Host, and the status page lists what is implemented, partial and planned.

Two ways to write a screen

The crate takes two vocabularies in rsx!, and both are always there. No feature flag turns either one off, because the owner decided that one crate serves both kinds of app.

You writeImportToday
HTML elements and CSS: div, span, input, a stylesheetdioxus_compose::html::prelude::*partial laid out and planned in the Host, not drawn yet
Compose widget names: Column, Text, Buttondioxus_compose::prelude::*implemented drawn through the renderer today

The HTML path is the centre of this guide, because most Dioxus apps are written that way. The widget path has its own pages under Compose widgets. An rsx! block uses one vocabulary or the other, since each prelude names its own elements. Putting both on one screen is planned.

What an app looks like

This is the whole of samples/hello: a component and its stylesheet. Nothing in it is specific to this crate except the import.

samples/hello/src/lib.rs, trimmed
use dioxus_compose::html::prelude::*;
use dioxus_hooks::use_signal;

pub const STYLE: &str = r#"
body { margin: 0; font-family: system-ui, sans-serif; font-size: 16px; line-height: 1.5; }
.page { padding: 40px 48px; }
h1 { margin: 0 0 16px; font-size: 32px; line-height: 40px; }
.link { color: #0969da; text-decoration: underline; cursor: pointer; }
"#;

pub fn app() -> Element {
    let mut hellos = use_signal(|| 0u32);

    rsx! {
        main { class: "page",
            h1 { id: "title", "Hello, Dioxus" }
            p { id: "action",
                a {
                    id: "greet",
                    class: "link",
                    href: "#",
                    onclick: move |event| {
                        event.prevent_default();
                        hellos += 1;
                    },
                    "Say hello"
                }
            }
        }
    }
}

How the HTML path works

Styles and layout are computed inside the Host, the Rust side, by blitz-dom running without a window. The renderer never sees CSS. It gets rectangles, colours and runs of text, which is all Compose needs to draw them.

  1. Run the app. HtmlDom runs your component in a Dioxus VirtualDom and applies its output to a blitz-dom document. Your root renders into <body>.
  2. Resolve styles. Stylo, the CSS engine from Firefox, resolves your stylesheets and the cascade.
  3. Lay out. Taffy places blocks, flex and grid containers and tables. Text is sized by a TextMeasurer you can supply (see Text).
  4. List what to draw. The result is a DisplayList: one entry per box, in paint order, with its rectangle, background, border, radii, shadows, clip, opacity, text runs, form field, image and scroll container.
  5. Plan the drawing. plan_from turns the list into a tree of drawing elements (boxes placed inside their containers, scroll containers, text, fields, images), and diff says what changed between two plans, node by node.
  6. Draw. The renderer draws the plan with Compose. planned This step is the missing bridge (#43).

Events travel the other way. A click at a point reaches the Dioxus handler of the element under it, and committed text from a field reaches its oninput. Today a test or an embedder calls these on HtmlDom directly, as the renderer will once it draws.

Rules the HTML path keeps

How close it is to a browser

The reference is Chromium, which is what VS Code draws with. On 2026-10-03 three regions of the VS Code workbench (the activity bar, the sidebar tree and the editor tabs) were laid out with the Code-OSS CSS at scale 1 on macOS. With text sized as VS Code measured it, the boxes came out as follows.

RegionBoxes within 1px of VS CodeLargest difference
Activity bar29 of 290px
Sidebar tree201 of 203 (99.0%)4.00px
Editor tabs94 of 940px

The test is dioxus-compose/tests/fr34_box_tolerance.rs. Text measured by Parley, the Host's own text engine, did not match: 6 of 22 line breaks fell in a different place. That is why text will be measured by Compose, the engine that draws it (partial, #43).

Why it exists

Desktop apps built on a browser engine are heavy, and an app that stays open all day pays for that every hour. Rust UI toolkits are light, but their text input does not yet reach what Compose does, and an input method that is right 90% of the time is an app you cannot type Korean in. dioxus-compose keeps the Dioxus and CSS that apps are already written in and borrows Compose for the pixels, the text and the input method.

The Compose side is compiled ahead of time into a native library, so there is no JVM in the shipped program. The notepad sample built with the widget path is one 28.76 MB file on macOS, measured on an M1.

Where to go next