dioxus-compose guide

Scrolling and overlays

Scroll containers, clips and layers

overflow decides what scrolls and what clips, and positioning decides what sits over the page. The Host lays out each scroll container's full content once; the renderer scrolls it, because a scroll position owned by Rust would always be a frame behind.

Scroll containers

A box with overflow: auto or scroll on an axis scrolls on that axis. Its display-list entry carries a scroll container: the viewport you see through, the size of everything inside, and which axes scroll. overflow: hidden on the other axis clips without scrolling. Every box inside records its nearest scrolling ancestor, so the renderer knows what moves with each scroll position.

samples/chat/src/lib.rs
.chat { display: flex; flex-direction: column; height: 100%; }

.messages {
    flex: 1;
    min-height: 0;
    overflow-x: hidden;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 12px;
}

.composer { display: flex; gap: 8px; padding: 12px; border-top: 1px solid #e2e8f0; }

In samples/chat the message list takes the room above the composer and scrolls vertically only. min-height: 0 matters here, as it does in a browser: without it a flex item will not shrink below its content, and the list would push the composer off the bottom.

samples/table scrolls the other way. Its table is at least 640px wide, so on a 480px page the 432px wrapper scrolls sideways through all 640px.

samples/table/src/lib.rs
.table-wrap {
    overflow-x: auto;
    overflow-y: hidden;
    border-radius: 8px;
}

.invoices { width: 100%; min-width: 640px; border-collapse: collapse; }

A conversation that grows

An assistant's reply arrives a piece at a time. samples/chat keeps the conversation in signals provided as context, and Chat::receive appends each piece to the last message. Only that message's entry changes, in its text and its size. Nothing else in the list is sent again.

samples/chat/src/lib.rs, trimmed
pub fn receive(mut self, chunk: &str) {
    let mut messages = self.messages.write();
    if let Some(last) = messages
        .last_mut()
        .filter(|last| last.from == Sender::Assistant)
    {
        last.text.push_str(chunk);
        return;
    }
    // A piece that arrives after the user has spoken starts a new reply.
}

The test appends ", where to?" to "Sure" and checks that the layout diff names one changed entry, the reply, with no removals and no change of order.

Clipping

A box whose overflow is anything but visible clips its descendants to its padding box, with its rounded corners. Each descendant's entry records the clip its ancestors impose, and a click outside that clip does not reach it. In samples/overlay a 400 by 160 banner inside a 240 by 80 preview is cut to the preview, and a click where the banner would have been, outside the preview, misses it.

samples/overlay/src/lib.rs
.preview {
    width: 240px;
    height: 80px;
    overflow: hidden;
    border-radius: 8px;
}
.preview-banner { width: 400px; height: 160px; background-color: #6366f1; }

Menus and dialogs

An overlay is ordinary HTML: show it with if, place it with positioning, and stack it with z-index. Paint order and hit-testing both follow the stacking, so a click on the backdrop reaches the backdrop and not the button under it.

samples/overlay/src/lib.rs, trimmed
if show_dialog {
    div { id: "backdrop", class: "backdrop", onclick: move |_| dialog_open.set(false) }
    div { id: "dialog", class: "dialog", role: "dialog",
        h2 { id: "dialog-title", "Delete project?" }
        p { "This cannot be undone." }
        div { class: "actions",
            button { id: "cancel", onclick: move |_| dialog_open.set(false), "Cancel" }
            button {
                id: "confirm",
                class: "danger",
                onclick: move |_| {
                    deleted.set(true);
                    dialog_open.set(false);
                },
                "Delete"
            }
        }
    }
}
samples/overlay/src/lib.rs
.backdrop {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 100;
    background-color: #0f172a;
    opacity: 0.5;
}

A position: fixed box is placed against the viewport wherever it sits in the document, so the backdrop covers the whole window and the dialog is centred in it.

Not yet