Toolbar
Toolbar is a transparent horizontal container for actions — buttons, separators, and short labels — used inside panel headers, tab strips, and custom command surfaces. The surrounding container owns its background and border.
The design mirrors the toolbars found in native UI frameworks: macOS NSToolbar and Windows ToolStrip.
#Import
use gpui_kit::component::toolbar::Toolbar;// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Toolbar, ToolbarSeparator, ToolbarGroup, ToolbarSpacer } from "toolbar.slint";use iced::widget::{column, container, mouse_area, scrollable, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};#Composition
Use child for Sizable controls. Toolbar applies its final size to those controls even when .small() or another size method appears after them in the builder chain. Use content for strings, separators, flexible spacers, and custom layout that should keep its own dimensions. Items render in source order.
- For a command, pass a
Button; Toolbar applies its size and forces the quietghost + compactpresentation. Chainlabel,icon,tooltip,on_click, etc. as needed. - For an icon-only button, always add a
tooltip; it is the accessible name as well. - For a separator, use
contentwithSeparator::vertical()and an explicit height. - For a non-interactive label, use one of the content methods with a plain string.
#Usage
#Commands
Toolbar::new("toolbar")
.child(
Button::new("new")
.icon(IconName::Plus)
.label("New")
.on_click(|_, window, cx| { /* ... */ }),
)
.content(Separator::vertical().h_5())
.child(
Button::new("undo")
.icon(IconName::Undo2)
.tooltip("Undo")
.on_click(|_, window, cx| { /* ... */ }),
)
.content(div().flex_1())
.child(
Button::new("more")
.icon(IconName::Ellipsis)
.tooltip("More options")
.on_click(|_, window, cx| { /* ... */ }),
)Rectangle {
horizontal-stretch: 1;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.sidebar;
Toolbar {
inset: 6px;
Button {
text: "New";
icon: "plus";
variant: ButtonVariant.ghost;
}
ToolbarSeparator {}
Button {
icon: "arrow-left";
variant: ButtonVariant.ghost;
}
Button {
icon: "arrow-right";
variant: ButtonVariant.ghost;
}
ToolbarSpacer {}
Button {
icon: "ellipsis";
variant: ButtonVariant.ghost;
}
}
}// A toolbar is transparent, so this frame stands in for the panel header
// that would supply the surface. The bar's spacer keeps the leading
// commands together and still lets the overflow reach the trailing edge.
let commands = body_box(surface(
Toolbar::new()
.size(ControlSize::Small)
.inset(INSET)
.child(command("New", ControlSize::Small, PLUS))
.content(ToolbarSeparator::new())
.child(icon_command("Undo", ControlSize::Small, ARROW_LEFT))
.child(icon_command("Redo", ControlSize::Small, ARROW_RIGHT))
// A command with no handler: the bar must keep it legible as
// disabled in both themes, which is the state a screenshot checks.
.child(disabled_command("Publish", ControlSize::Small, UPLOAD))
.content(ToolbarSpacer::new())
.child(overflow(app, ControlSize::Small)),
false,
));#Sizes
// The size is the whole bar's: it sets the height and the command slots,
// and the caller sizes the controls inside it to match.
let sized = |size: ControlSize, label: &'static str| {
surface(
Toolbar::new()
.size(size)
.inset(INSET)
.child(
Button::new(label)
.variant(Variant::Ghost)
.size(size)
.on_press(Message::Command(label)),
)
.child(
Button::new(label)
.variant(Variant::Ghost)
.size(size)
.on_press(Message::Command(label)),
)
.content(ToolbarSpacer::new()),
false,
)
};
let sizes = column![
sized(
ControlSize::XSmall,
// The Slint example's own labels: the same word twice at each step.
"Small",
),
sized(ControlSize::Medium, "Medium"),
sized(ControlSize::Large, "Large"),
]
.spacing(12.0);Use Sizable to change the container height, spacing, text size, and hosted controls together: xsmall (28px), small (32px, default), and medium (48px). Builder order does not matter.
Toolbar::new("toolbar")
.child(Button::new("new").icon(IconName::Plus).label("New"))
.child(Button::new("find").icon(IconName::Search).tooltip("Find"))
.small()VerticalLayout {
spacing: 12px;
Rectangle {
horizontal-stretch: 1;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.sidebar;
Toolbar {
inset: 6px;
size: ControlSize.xsmall;
Button {
text: "Small";
variant: ButtonVariant.ghost;
size: ControlSize.xsmall;
}
Button {
text: "Small";
variant: ButtonVariant.ghost;
size: ControlSize.xsmall;
}
ToolbarSpacer {}
}
}
Rectangle {
horizontal-stretch: 1;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.sidebar;
Toolbar {
inset: 6px;
size: ControlSize.medium;
Button {
text: "Medium";
variant: ButtonVariant.ghost;
}
Button {
text: "Medium";
variant: ButtonVariant.ghost;
}
ToolbarSpacer {}
}
}
Rectangle {
horizontal-stretch: 1;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.sidebar;
Toolbar {
inset: 6px;
size: ControlSize.large;
Button {
text: "Large";
variant: ButtonVariant.ghost;
size: ControlSize.large;
}
Button {
text: "Large";
variant: ButtonVariant.ghost;
size: ControlSize.large;
}
ToolbarSpacer {}
}
}
}#Labels and custom elements
Toolbar::new("toolbar")
.content("Dashboard")
.content(Separator::vertical().h_5())
.content(
h_flex()
.items_center()
.gap_1()
.child(Icon::new(IconName::CircleCheck).xsmall())
.child("Saved"),
)
.content(div().flex_1())
.child(Button::new("settings").icon(IconName::Settings2).tooltip("Settings"))Rectangle {
horizontal-stretch: 1;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.sidebar;
Toolbar {
inset: 6px;
spacing: 8px;
Text {
text: "3 files changed";
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
vertical-alignment: center;
}
ToolbarSeparator {}
Input {
width: 200px;
size: ControlSize.small;
placeholder: "Filter files…";
prefix-icon: "search";
}
ToolbarSpacer {}
Button {
text: "Review";
variant: ButtonVariant.primary;
}
}
}// A label and a field are content, not commands, so they keep their own
// dimensions and go through `content`; only `Review` is a command.
let labels = body_box(surface(
Toolbar::new()
.size(ControlSize::Small)
.inset(INSET)
.spacing(theme::SPACING_SM)
.content(muted_line("3 files changed"))
.content(ToolbarSeparator::new())
.content(
Input::new("toolbar-filter")
.size(ControlSize::Small)
.width(FILTER_WIDTH)
.placeholder("Filter files…")
.prefix(glyph_muted(SEARCH))
.value(app.filter.clone())
.on_input(Message::Filtered),
)
.content(ToolbarSpacer::new())
.child(
Button::new("Review")
.variant(Variant::Primary)
.size(ControlSize::Small)
.on_press(Message::Command("Review")),
),
false,
));#Custom styling
// The surrounding surface owns the background and the border, so the bar
// drops into a header without fighting it; a tinted surface is the one the
// Slint example washes with the primary colour.
let custom = body_box(surface(
Toolbar::new()
.size(ControlSize::Small)
.inset(INSET)
.spacing(6.0)
.content(
text("Editor")
.size(theme::FONT_SIZE_XS)
.font(iced::Font {
weight: iced::font::Weight::Medium,
..iced::Font::DEFAULT
})
.style(|theme: &Theme| text::Style {
color: Some(Tokens::of(theme).foreground),
}),
)
.content(ToolbarSpacer::new())
.child(icon_command("Toggle left panel", ControlSize::Small, PANEL_LEFT))
.child(icon_command(
"Toggle right panel",
ControlSize::Small,
PANEL_RIGHT,
)),
true,
));Toolbar is transparent and borderless by default. It implements Styled, so a standalone command surface can add its own appearance.
Toolbar::new("toolbar")
.bg(cx.theme().secondary)
.border_color(cx.theme().border)
.content("Ready")Rectangle {
horizontal-stretch: 1;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.primary.with-alpha(Theme.dark ? 0.18 : 0.08);
Toolbar {
inset: 6px;
spacing: 6px;
Text {
text: "Editor";
color: Theme.foreground;
font-size: Theme.font-size-xs;
font-weight: 600;
vertical-alignment: center;
}
ToolbarSpacer {}
Button {
icon: "panel-left";
variant: ButtonVariant.ghost;
}
Button {
icon: "panel-right";
variant: ButtonVariant.ghost;
}
}
}#Groups
// A group holds a run together with a tighter gap than the bar's, so the
// three runs read as three units. The separator divides them; the spacer
// pushes the last run to the trailing edge.
let groups = body_box(surface(
Toolbar::new()
.size(ControlSize::Small)
.inset(INSET)
.child(
ToolbarGroup::new()
.size(ControlSize::Small)
.child(icon_command("Undo", ControlSize::Small, ARROW_LEFT))
.child(icon_command("Redo", ControlSize::Small, ARROW_RIGHT)),
)
.content(ToolbarSeparator::new())
.child(
ToolbarGroup::new()
.size(ControlSize::Small)
.child(icon_command("Bold", ControlSize::Small, BOLD))
.child(icon_command("Italic", ControlSize::Small, ITALIC))
.child(icon_command("Underline", ControlSize::Small, UNDERLINE)),
)
.content(ToolbarSpacer::new())
.child(
ToolbarGroup::new()
.size(ControlSize::Small)
.child(
Button::new("Share")
.variant(Variant::Ghost)
.size(ControlSize::Small)
.on_press(Message::Command("Share")),
)
.child(icon_command("More", ControlSize::Small, ELLIPSIS)),
),
false,
));Wrap related controls in ToolbarGroup to give them an accessible name, so assistive technology reads a run of controls as one unit. The group implements Sizable, and a parent Toolbar propagates its final size through the group to every control:
use gpui_kit::component::toolbar::ToolbarGroup;
Toolbar::new("document-toolbar")
.child(
ToolbarGroup::new("history-group")
.label("History")
.gap_2() // match the bar's own item spacing
.child(Button::new("undo").icon(IconName::Undo2).tooltip("Undo"))
.child(Button::new("redo").icon(IconName::Redo2).tooltip("Redo")),
)Rectangle {
horizontal-stretch: 1;
border-radius: Theme.radius-md;
border-width: 1px;
border-color: Theme.border;
background: Theme.sidebar;
Toolbar {
inset: 6px;
ToolbarGroup {
label: "History";
Button {
icon: "arrow-left";
variant: ButtonVariant.ghost;
}
Button {
icon: "arrow-right";
variant: ButtonVariant.ghost;
}
}
ToolbarSeparator {}
ToolbarGroup {
label: "Formatting";
Button {
icon: "bold";
variant: ButtonVariant.ghost;
}
Button {
icon: "italic";
variant: ButtonVariant.ghost;
}
Button {
icon: "underline";
variant: ButtonVariant.ghost;
}
}
ToolbarSpacer {}
ToolbarGroup {
label: "Document";
Button {
text: "Share";
variant: ButtonVariant.ghost;
}
Button {
icon: "ellipsis";
variant: ButtonVariant.ghost;
}
}
}
}Unlike Base UI’s Toolbar.Group, a group cannot disable its children: that API propagates through React context into Base UI’s own button primitives, which has no equivalent for arbitrary GPUI children. Disabling the hosted controls is the caller’s job.
Separators and other non-sized elements use content. Sized controls use child so the toolbar can propagate its size.
#Keyboard
The toolbar exposes Toolbar semantics to assistive technology and owns roving keyboard focus, matching the ARIA toolbar pattern and Base UI’s Toolbar:
| Key | Behavior |
|---|---|
← / → | Move focus to the previous / next control (horizontal toolbar) |
↑ / ↓ | Move focus to the previous / next control (vertical toolbar) |
Tab | Enter or leave the toolbar; the bar itself is not a tab stop |
Focus wraps around at the ends. Hosted inputs keep their own arrow-key caret behavior; place inputs at the trailing end of the bar. This behavior comes from the unstyled gpui_base::Toolbar primitive, so applications building custom toolbars on the base layer get the same contract.
#Notes
- Use
content(div().flex_1())when later items need to align to the trailing edge. - Keep the primary command visible; move low-frequency actions into a dropdown or overflow menu rather than hiding them behind hover.
- Toolbar has no default background or border; its host surface supplies them.
#API Reference
#Toolbar
| Method | Description |
|---|---|
new() | Create a new, empty toolbar (small size) |
child(c) / children(cs) | Add sized control(s) in source order |
content(c) / contents(cs) | Add non-sized content in source order |
with_size(size) | Set the bar size — xsmall, small, or medium |
disabled(value) | Disable roving navigation; the owner also disables hosted controls |
Control methods require Sizable + IntoElement; content methods accept general elements. Toolbar also implements Styled and Sizable.
#Toolbar
A transparent row of commands. Mirrors GPUI Component's `Toolbar`: it owns no background and no border, so the surrounding surface (a panel header, a tab strip, a custom command bar) supplies them. Items render in source order and take the toolbar's size. End the row with a `ToolbarSpacer`. A layout with unclaimed space spreads its children apart, so a spacer is what keeps the commands at the leading edge and still lets a trailing group sit at the far end.
| Name | Kind | Type | Default | Description |
|---|---|---|---|---|
size | in | ControlSize | ControlSize.small | |
spacing | in | length | 4px | The gap between items. A separator draws its own rule, so it wants less. |
inset | in | length | 0px | The inset from the surface. A toolbar drops into a header that already has its own padding, so this defaults to none. |
#ToolbarSeparator
The vertical rule between command groups. Give it a height; the default reads as a divider in a small toolbar.
| Name | Kind | Type | Default |
|---|---|---|---|
bar-height | in | length | 18px |
#ToolbarGroup
A run of related controls. GPUI's `ToolbarGroup` gives the run an accessible name so assistive technology reads it as one unit; Slint has no toolbar role, so the group takes `groupbox` and carries the label itself. Like GPUI's, a group cannot disable its children -- that API propagates through React context there, which has no equivalent for arbitrary children here.
| Name | Kind | Type | Default | Description |
|---|---|---|---|---|
label | in | string | The accessible name for the run, e.g. "History". |
#ToolbarSpacer
A pushed group: everything after it sits at the trailing edge.