Patterns
How window structure such as the toolbar, sidebar, inspector and titlebar is declared once and drawn by each OS.
Declare the structure of a window with intents (scaffold, toolbar, sidebar, inspector, titlebar) and let the OS draw it. On macOS each one is the system's own component, so a new macOS release restyles the app with no change here. Do not rebuild a sidebar or toolbar from divs; a pixel copy goes stale the day the OS changes. The Chrome example declares a Mail-style window and names no AppKit class.

Captured on macOS 27.0, 2026-10-14.
What each intent becomes
| Intent | macOS (AppKit) | Linux and Windows (drawn through GPUI) |
|---|---|---|
scaffold().sidebar() | sidebar split item hosting a source list | pane on the preset's sidebar surface |
scaffold().inspector() | inspector split item | right pane |
toolbar() | NSToolbar with system items and native item views | header row |
titlebar() | content under a transparent titlebar, inset for the traffic lights | inset for the caption buttons or the compositor's button-layout |
window_controls() | the real traffic lights | caption buttons only under client-side decorations |
.context_menu() | NSMenu | menu drawn in the overlay stack |
Alert | NSAlert, a sheet on the window | dialog with a scrim |
menu bar (set_menus) | global NSMenu | no bar: the items are folded into the command palette (the web draws a bar above its canvas) |
| settings window | toolbar-tab window with grouped rows, cmd-, | separate window |
glass() | NSGlassEffectView on macOS 26 and later, NSVisualEffectView before | the preset's material: Acrylic on Windows, a transparent surface the compositor may blur on Linux |
On Windows the drawn chrome follows WinUI, checked against Windows 11's own apps (Settings, Task Manager): the sidebar and toolbar are one Mica surface with no divider, the content sits on an opaque layer whose leading corner takes the window's radius, the sidebar draws like NavigationView (Segoe Fluent icons, a subtle fill with an accent pill on the selected row) and its toggle folds it to a 48 px icon rail, the caption buttons are as tall as the toolbar, the window title is set in the 12 pt caption style, and banner is an inset card like InfoBar. Form controls, dialogs and menus still use the Adwaita look there. Linux is checked on Hyprland.
Scaffold, toolbar, sidebar and inspector
scaffold() takes a toolbar, a sidebar, the content and an inspector. Every part is optional. The toolbar holds system items (sidebar_toggle, inspector_toggle, flexible_space) next to your own buttons, and macOS gives them the system look and sizing.
.toolbar(
toolbar()
.title("Inbox")
.sidebar_toggle()
.item(icon_button("new", Icon::SquarePen).tooltip("New Message").dispatch(NewMessage))
.item(icon_button("delete", Icon::Trash).tooltip("Delete").dispatch(DeleteMessage))
.flexible_space()
.inspector_toggle(),
)
From examples/chrome/src/lib.rs, line 64.
The sidebar holds a source_list: sections of items with an icon and badge, the selection owned by your state. On macOS it is an NSOutlineView in source-list style.
.sidebar(
sidebar().min_w(px(180.)).child(
source_list("folders")
.section(
"Mailboxes",
[
source_item("inbox", "Inbox").icon(Icon::Inbox).badge("12"),
source_item("archive", "Archive").icon(Icon::Archive),
source_item("trash", "Trash").icon(Icon::Trash),
],
)
.section("Smart", [source_item("starred", "Starred").icon(Icon::Star)])
.selected(self.folder.clone())
.on_select(cx.listener(|this, e: &SourceSelected, _, cx| {
this.folder = e.id.clone();
cx.notify();
})),
),
)
From examples/chrome/src/lib.rs, line 75.
The inspector collapses from state and its toggle sits in the toolbar:
.inspector(
inspector()
.collapsed(!self.inspector)
.min_w(px(200.))
.child(v_flex().p_3().child(text("Details").text_color(theme.text.secondary))),
)
From examples/chrome/src/lib.rs, line 107.
Key equivalents come from the bindings, not from the menu items: cx.bind_keys registers secondary-n, and secondary is cmd on macOS and ctrl elsewhere. set_menus builds the menu bar and shows each item's shortcut from the same binding.
cx.bind_keys([
KeyBinding::new("secondary-n", NewMessage, Some("Mail")),
KeyBinding::new("secondary-backspace", DeleteMessage, Some("Mail")),
KeyBinding::new("secondary-o", OpenFile, Some("Mail")),
KeyBinding::new("secondary-,", OpenSettings, None),
KeyBinding::new("secondary-alt-i", ToggleInspector, Some("Mail")),
]);
set_menus(
cx,
[
Menu::app([MenuItem::action("Settings…", OpenSettings)]),
Menu::new("File")
.items([MenuItem::action("New Message", NewMessage), MenuItem::action("Attach File…", OpenFile)]),
Menu::new("Message").items([MenuItem::action("Delete", DeleteMessage).destructive()]),
Menu::new("View").items([MenuItem::action("Show Inspector", ToggleInspector)]),
],
);
From examples/chrome/src/lib.rs, line 193.
Titlebar and window controls
A window with Titlebar::Native gets the OS titlebar, or the scaffold's toolbar in it. For a custom row, open the window with Titlebar::Custom and build it with titlebar(), which reserves the room the OS controls need: the traffic lights on the left on macOS, the caption buttons on the right on Windows, GNOME's button-layout on Linux, and nothing on Hyprland, where the compositor draws no controls. It updates on fullscreen and right-to-left layouts.
// Pattern override: a custom titlebar even on macOS. `titlebar()` reserves the OS
// controls' side and size (traffic lights left on macOS, caption buttons right on
// Windows, nothing on Hyprland), so no margin is tuned by hand.
.child(
titlebar()
.title("Overrides")
.trailing(
// Per platform: glass bezel on macOS, accent style on Windows, pill on Linux.
button("share", "Share")
.icon(Icon::Share)
.dispatch(Share)
.on_macos(|b| b.bezel(Bezel::Glass))
.on_windows(|b| b.accent())
.on_linux(|b| b.rounded_full().px_4())
.on_web(|b| b.outline()),
)
// Coordinates instead of the default slot, macOS only.
.on_macos(|t| t.controls(window_controls().at(point(px(18.), px(20.))))),
)
From examples/overrides/src/lib.rs, line 23.
window_controls() places the real buttons. With no argument they take the slot titlebar() reserves. .at(point(..)) pins them at window coordinates, which Hello uses to put the traffic lights where its layout wants them:
.child(window_controls().at(point(px(16.), px(10.))))
From examples/hello/src/lib.rs, line 25.
.embedded() makes the controls a layout slot instead: the backend moves the real buttons onto the slot's rectangle on every layout pass, so they ride along with a sliding sidebar. The buttons stay the OS's, with their hover group, menu and accessibility. On Linux they are drawn only when the window has client-side decorations, so the same code shows none on Hyprland.
Alerts, sheets and pickers
window.alert shows an Alert and calls back with the index of the pressed button. By default it is attached to the window as a sheet on macOS; .modal() shows it in front of the whole app. window.open_dialog opens the system file picker.
fn delete(&mut self, _: &DeleteMessage, window: &mut Window, cx: &mut Context<Self>) {
let this = cx.weak_entity();
let alert =
Alert::new("Delete this message?").message("It moves to the Trash.").destructive("Delete").cancel("Cancel");
window.alert(cx, alert, move |answer, cx| {
let _ = this.update(cx, |this, cx| {
this.status = if answer.0 == 0 { "Deleted".into() } else { "Kept".into() };
cx.notify();
});
});
}
From examples/chrome/src/lib.rs, line 20.
Do not block in the callback: it runs after the dialog closes, with the app free to handle events.
Settings
open_settings opens the app's settings window, or brings the open one forward. On macOS it is the system preferences shape: a toolbar of tabs over grouped rows, opened with cmd-, when the app binds OpenSettings. The tabs are a toolbar().tab(..), the rows a form of form_section and form_row:
.child(
toolbar()
.tab("general", "General", Icon::Settings)
.tab("accounts", "Accounts", Icon::Inbox)
.selected_tab(self.tab.clone())
.on_tab(cx.listener(|this, e: &TabSelected, _, cx| {
this.tab = e.id.clone();
cx.notify();
})),
)
From examples/chrome/src/lib.rs, line 165.
For settings that persist to a file, settings_form builds the whole window from a settings struct; see settings and config.
Glass
glass() is a container that maps to the system material and never to a replica. It takes .variant(GlassVariant::Regular | Clear), .tint(color) and .interactive(), and glass_group() merges neighbouring glass where the OS does. It takes every override like any other element. The Overrides example tints one:
// Subtree theme override: everything inside uses a warm accent and larger radii.
.child(
glass()
.variant(GlassVariant::Clear)
.tint(theme::warm_glass_tint())
.rounded_xl()
.p_4()
.gap_2()
.theme(theme::warm)
.child(text("Tinted glass with its own accent"))
.child(
h_flex().gap_2().child(button("ok", "Accept").primary()).child(button("no", "Decline")),
)
// Pattern override on one element: this context menu is drawn, not an NSMenu.
.context_menu(|menu, _window, _cx| menu.item("Rename…", Rename).item("Share", Share))
.pattern(Pattern::Drawn),
),
From examples/overrides/src/lib.rs, line 82.
Changing a pattern
If a pattern is wrong for your app, Overrides shows how to swap one for a drawn version. If it looks wrong on an OS, that is a bug in the backend: the fix goes there so every app benefits, not in a per-app margin.