Skip to content

Design Layout Rules

MeowLynxSea edited this page Jun 13, 2026 · 4 revisions

Layout rules

The headless layer is layout-agnostic — every factory hands a styled Div back to the caller, and the caller decides the surrounding flex / grid / positioning. These are the rules the demos converge on to keep things predictable.

ListItem default

ListItem (in yororen-ui-core/src/headless/list_item.rs) is a row content container. Its default behaviour:

  • Do not stretch children horizontally.
  • Only opt into flexible growth when the caller explicitly wants it.

This prevents inputs / buttons / selects from unexpectedly expanding to full width when placed in a row container. If a row needs to fill its container, the caller opts in with .w_full() on the row itself.

use gpui::div;
use yororen_ui::headless::button::button;
use yororen_ui::headless::list_item::list_item;
use yororen_ui::headless::text_input::text_input;

let row = list_item("row-1", "Alice", cx)
    .content(
        div()
            .flex()
            .items_center()
            .gap_2()
            .child(text_input("name").placeholder("Name"))
            .child(button("save", cx).caption("Save")),
    );

Flex row gaps

When stacking buttons in a toolbar, use a flex_row with an explicit gap. The default gap is 0, so buttons will butt against each other unless you say otherwise.

use gpui::{div, px};

let toolbar = div()
    .flex()
    .flex_row()
    .gap(px(8.))
    .child(button("new", cx).caption("New"))
    .child(button("open", cx).caption("Open"))
    .child(button("save", cx).caption("Save"));

Modals live at the scroll root

A modal must be rendered at the scroll-root level — sibling to your main content, not inside it — and wrapped in gpui::deferred(...).with_priority(2) so it paints above the page content but below the toast host. The gallery_demo shows the exact placement.

use gpui::deferred;

let content = div()
    .child(/* your main app content */);

let modal_host = modal("settings", app.modal_state.clone())
    .child(/* ... */)
    .render(cx);

// (your app's custom notification host, see
//  gallery_demo/src/notifications_host.rs::deferred_host for the
//  canonical v0.3 pattern; the `notification_host()` factory in
//  yororen-ui-default-renderer is dead code and not a public API)
let toast_host = /* your deferred host, priority 3 */;

deferred(content.with_priority(0))
    .with_priority(0)
    // deferred priority wraps in the same way — see gallery_demo

Toasts above modals

The notification host goes on top of modals via gpui::deferred(...).with_priority(3). The priority ladder is:

Layer Priority
App content 0
Modal scrim + body 2
Toast / notification host 3

Avoid .left_* / .right_* in RTL

If your custom layout uses .left_* / .right_* directly instead of the start / end helpers, it will render wrong in RTL locales. Use the rtl module — see Design-Internationalization.

Width sizing

headless::*::render(cx) returns a Stateful<Div> that does not impose a default width. The caller's parent decides. Common patterns:

use gpui::div;

let full_width = div().w_full().child(button("save", cx).caption("Save"));
let auto_width = div().child(button("save", cx).caption("Save")); // shrink-to-fit
let fixed = div().w(px(120.)).child(button("save", cx).caption("Save"));

Related pages

Clone this wiki locally