dioxus-compose guide

Layout and CSS

The CSS you already write

Layout is blitz-dom's: Stylo resolves the CSS and Taffy places the boxes, the same engines Dioxus's own native renderer uses. So the support is broad, and this page shows it through the examples rather than as a list of properties.

Stylesheets

Stylesheets go in HtmlConfig::stylesheets, applied in order after the user-agent sheet, as if each were a <style> in <head>. The style attribute works too. The user-agent sheet is blitz-dom's, much like a browser's (body { margin: 8px }), and HtmlConfig::user_agent_stylesheet replaces it when you want a blank start.

let dom = HtmlDom::with_config(app, HtmlConfig {
    stylesheets: vec![STYLE.to_string()],
    ..HtmlConfig::default()
});

Block and inline flow

Blocks stack and their vertical margins collapse. In samples/hello the heading's 16px bottom margin and the paragraph's 12px top margin collapse to 16px, so the paragraph sits at y = 96 rather than 108. A paragraph that mixes plain text, strong and em is one inline formatting context with a run per style, all on one line.

samples/hello/src/lib.rs
p { id: "intro",
    "This page is "
    strong { id: "bold", "HTML" }
    " and "
    em { id: "italic", "CSS" }
    ", laid out by the Host."
}

Flexbox

samples/dashboard is flexbox from top to bottom: a column for the page, a row for the top bar, a column for the sidebar and a wrapping row of cards. In a 1000px viewport three 200px cards fit a row; at 700px the row holds two and the rest wrap. Its tests check both.

samples/dashboard/src/lib.rs
.app { display: flex; flex-direction: column; height: 100vh; }

.topbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-shrink: 0;
    height: 56px;
}

.body { display: flex; flex: 1; align-items: stretch; min-height: 0; }

.cards {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    justify-content: flex-start;
    align-items: flex-start;
}

min-width: fit-content works as well. In samples/workbench a tab is 120px wide unless its name needs more, so layout_engine.rs gets 185px while main.rs keeps 120px.

Grid

samples/gallery uses repeat(auto-fill, minmax(160px, 1fr)). In an 800px viewport that is four 183px columns, and a tile with grid-column: span 2 covers two of them (378px).

samples/gallery/src/lib.rs
.grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    grid-auto-rows: 140px;
    gap: 12px;
    padding: 16px;
}

.tile.wide { grid-column: span 2; }

Tables

table, thead, tbody, tr, th and td lay out as a table. In samples/table the column widths come from the header cells, border-collapse: collapse shares borders, and text-align: right puts the amounts against the right edge. The table is at least 640px wide, so on a 480px page it scrolls sideways inside its wrapper (see Scrolling and overlays).

Positioning and stacking

position: relative, absolute and fixed work, and z-index decides both the paint order and which box a click lands on. In samples/overlay a menu is placed at top: 100% of its button's wrapper and paints over the content below it, and a dialog is fixed to the centre of the viewport over a fixed backdrop.

samples/overlay/src/lib.rs
.menu-anchor { position: relative; display: flex; }
.menu { position: absolute; top: 100%; left: 0; z-index: 10; width: 180px; }

.dialog {
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 101;
    width: 320px;
    transform: translate(-50%, -50%);
}

The dialog's translate(-50%, -50%) works because translations are added to the box's position. Other transforms are not drawn yet; see below.

Backgrounds, borders and effects

Every box carries its resolved decorations into the display list: background colour and background images (Images and backgrounds), borders per side with their own widths and colours, corner radii reduced the way CSS reduces radii that would overlap, box-shadow, and opacity multiplied down through its ancestors. visibility: hidden keeps a box's place and draws nothing, and display: none removes it.

samples/dashboard/src/lib.rs
.card {
    width: 200px;
    height: 120px;
    padding: 16px;
    background: #ffffff;
    border: 1px solid #d0d7de;
    border-radius: 12px;
    box-shadow: 0 1px 3px rgba(31, 35, 40, 0.2);
}

.card.alert { border-left: 4px solid #cf222e; }
.card.stale { opacity: 0.6; }

What the renderer will receive

The renderer does not get CSS. plan_from turns the display list into drawing elements, and the choices it makes keep that tree small:

Not yet

CSSTodayStatus
transform: rotate, scale, skew, matrix(), transform-originStylo computes them; only translations reach the box. A box with any other transform stays where layout put it.planned #46
transition, animation, @keyframesEvery animated value stays at its first value.planned #46 (opacity, transform, colours), #47 (width, height, margins)
Page zoom like VS Code's (Cmd/Ctrl + +)The page is laid out at the scale you pass to layout.planned

Animations will be split by what they move. Properties that do not change layout (opacity, transforms, colours) will play in the renderer on Compose's own animation clock, so the Host does no work per frame. Properties that do change layout will be recomputed by the Host each frame, because only the layout engine can say where everything else moves.