Skip to content

Design Layout Rules

MeowLynxSea edited this page Jun 18, 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.

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.

root
├── scroll root (overflow_y_scroll)
│   └── page content
├── modal scrim + panel          (deferred priority 2)
└── notification host            (deferred priority 3)
use gpui::deferred;

let scroll_root = div().size_full().id("page-scroll").overflow_y_scroll().child(/* page */);
let modal_el = modal("settings", state).child(/* … */).render(cx);

div().size_full()
    .child(scroll_root)
    .child(deferred(modal_el).with_priority(2))

Toasts above modals

The notification host sits at priority 3, above modals (2) and popovers (1).

.absolute() doesn't float

gpui's .absolute() removes an element from layout flow but paint order is still DOM order. A later sibling will paint on top of an earlier .absolute() element. To float a popover / dropdown / tooltip above its trigger, use gpui::deferred(child).with_priority(N).

let root = div().relative().child(trigger);
if open {
    let menu = div().absolute().top(px(8.)).left_0().bg(/* … */).child(/* … */);
    root.child(gpui::deferred(menu).with_priority(1))
} else {
    root
}

Avoid .left_* / .right_* in RTL

If your custom layout uses .left_* / .right_* directly instead of the start / end helpers (or gpui's built-in .start_* / .end_*), it will render wrong in RTL locales. See the rtl module — cx.i18n().is_rtl() tells you which direction is active.

Width sizing

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

let full_width = div().w_full().child(button("save", cx).caption("Save"));     // fill the parent
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"));   // fixed

See also

Clone this wiki locally