Tabs
A tabbed interface component for organizing content into separate sections. Supports multiple variants, sizes, navigation controls, and interactive features like reordering and prefix/suffix elements.
#Import
use gpui_kit::component::tab::{Tab, TabBar};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { TabsVariant, TabItem, Tabs } from "tabs.slint";use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};#Usage
#Basic Tabs
TabBar::new("tabs")
.selected_index(0)
.on_click(|selected_index, _, _| {
println!("Tab {} selected", selected_index);
})
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile"))
.child(Tab::new().label("Settings"))Tabs {
items: [
{ label: "Account", icon: "", disabled: false },
{ label: "Password", icon: "", disabled: false },
{ label: "Settings", icon: "", disabled: false },
];
selected: 0;
Text {
text: "Account settings live here.";
color: Theme.foreground;
}
}// The body is the active tab's content: the caller owns `selected` and hands
// the bar the body for that index, which is what the Slint component's
// `@children` slot holds.
const BASIC_BODY: [&str; 3] = [
"Account settings live here.",
"Change your password here.",
"Manage your settings here.",
];
let basic = Tabs::new([
Tab::new("Account").on_press(Message::Choose("basic", 0)),
Tab::new("Password").on_press(Message::Choose("basic", 1)),
Tab::new("Settings").on_press(Message::Choose("basic", 2)),
])
.variant(Variant::Underline)
.selected(app.basic)
.body(muted_line(BASIC_BODY[app.basic]));#Tab Variants
// The variant sets the whole bar: a rule under the active tab, a filled
// pill, or an outlined one.
let variants = column![
variant_bar(Variant::Underline, app.underline, "Underline."),
variant_bar(Variant::Pill, app.pill, "Pill."),
variant_bar(Variant::Outline, app.outline, "Outline."),
]
.spacing(16.0);#Default Tabs
TabBar::new("default-tabs")
.selected_index(0)
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile"))
.child(Tab::new().label("Documents"))#Underline Tabs
TabBar::new("underline-tabs")
.underline()
.selected_index(0)
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile"))
.child(Tab::new().label("Documents"))#Pill Tabs
TabBar::new("pill-tabs")
.pill()
.selected_index(0)
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile"))
.child(Tab::new().label("Documents"))#Outline Tabs
TabBar::new("outline-tabs")
.outline()
.selected_index(0)
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile"))
.child(Tab::new().label("Documents"))#Segmented Tabs
use gpui_kit::component::IconName;
TabBar::new("segmented-tabs")
.segmented()
.selected_index(0)
.child(IconName::Bot)
.child(IconName::Calendar)
.child(IconName::Map)
.children(vec!["Settings", "About"])VerticalLayout {
spacing: 16px;
alignment: start;
Tabs {
items: [
{ label: "Default", icon: "", disabled: false },
{ label: "Selected", icon: "", disabled: false },
{ label: "Third", icon: "", disabled: false },
];
selected: 1;
variant: TabsVariant.underline;
Text {
text: "Underline.";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
}
}
Tabs {
items: [
{ label: "Default", icon: "", disabled: false },
{ label: "Selected", icon: "", disabled: false },
{ label: "Third", icon: "", disabled: false },
];
selected: 1;
variant: TabsVariant.pill;
Text {
text: "Pill.";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
}
}
Tabs {
items: [
{ label: "Default", icon: "", disabled: false },
{ label: "Selected", icon: "", disabled: false },
{ label: "Third", icon: "", disabled: false },
];
selected: 1;
variant: TabsVariant.outline;
Text {
text: "Outline.";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
}
}
}#Tab Sizes
// Extra Small
TabBar::new("tabs").xsmall()
.child(Tab::new().label("Small"))
// Small
TabBar::new("tabs").small()
.child(Tab::new().label("Small"))
// Medium (default)
TabBar::new("tabs")
.child(Tab::new().label("Medium"))
// Large
TabBar::new("tabs").large()
.child(Tab::new().label("Large"))VerticalLayout {
spacing: 12px;
alignment: start;
Tabs {
items: [
{ label: "Small", icon: "", disabled: false },
{ label: "Tabs", icon: "", disabled: false },
];
selected: 0;
size: ControlSize.small;
variant: TabsVariant.pill;
}
Tabs {
items: [
{ label: "Medium", icon: "", disabled: false },
{ label: "Tabs", icon: "", disabled: false },
];
selected: 0;
size: ControlSize.medium;
variant: TabsVariant.pill;
}
Tabs {
items: [
{ label: "Large", icon: "", disabled: false },
{ label: "Tabs", icon: "", disabled: false },
];
selected: 0;
size: ControlSize.large;
variant: TabsVariant.pill;
}
}let sizes = column![
Tabs::new([
Tab::new("Small").on_press(Message::Choose("small", 0)),
Tab::new("Tabs").on_press(Message::Choose("small", 1)),
])
.variant(Variant::Pill)
.size(ControlSize::Small)
.selected(app.small),
Tabs::new([
Tab::new("Medium").on_press(Message::Choose("medium", 0)),
Tab::new("Tabs").on_press(Message::Choose("medium", 1)),
])
.variant(Variant::Pill)
.size(ControlSize::Medium)
.selected(app.medium),
Tabs::new([
Tab::new("Large").on_press(Message::Choose("large", 0)),
Tab::new("Tabs").on_press(Message::Choose("large", 1)),
])
.variant(Variant::Pill)
.size(ControlSize::Large)
.selected(app.large),
]
.spacing(12.0);#Tabs with Icons
use gpui_kit::component::{Icon, IconName};
TabBar::new("icon-tabs")
.child(Tab::default().icon(IconName::User).with_variant(TabVariant::Tab))
.child(Tab::default().icon(IconName::Settings).with_variant(TabVariant::Tab))
.child(Tab::default().icon(IconName::Mail).with_variant(TabVariant::Tab))Tabs {
items: [
{ label: "Account", icon: "user", disabled: false },
{ label: "Password", icon: "lock", disabled: false },
{ label: "Billing", icon: "circle", disabled: false },
];
selected: 0;
variant: TabsVariant.pill;
Text {
text: "An icon leads each label.";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
}
}let icons = Tabs::new([
Tab::new("Account")
.icon(tab_glyph(USER, app.icons == 0))
.on_press(Message::Choose("icons", 0)),
Tab::new("Password")
.icon(tab_glyph(LOCK, app.icons == 1))
.on_press(Message::Choose("icons", 1)),
Tab::new("Billing")
.icon(tab_glyph(CIRCLE, app.icons == 2))
.on_press(Message::Choose("icons", 2)),
])
.variant(Variant::Pill)
.selected(app.icons)
.body(muted_line("An icon leads each label."));#Tabs with Prefix and Suffix
use gpui_kit::component::button::Button;
use gpui_kit::component::{h_flex, IconName};
TabBar::new("tabs-with-controls")
.prefix(
h_flex()
.gap_1()
.child(Button::new("back").ghost().xsmall().icon(IconName::ArrowLeft))
.child(Button::new("forward").ghost().xsmall().icon(IconName::ArrowRight))
)
.suffix(
h_flex()
.gap_1()
.child(Button::new("inbox").ghost().xsmall().icon(IconName::Inbox))
.child(Button::new("more").ghost().xsmall().icon(IconName::Ellipsis))
)
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile"))
.child(Tab::new().label("Settings"))#Disabled Tabs
TabBar::new("tabs-with-disabled")
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile").disabled(true))
.child(Tab::new().label("Settings"))Tabs {
items: [
{ label: "Enabled", icon: "", disabled: false },
{ label: "Disabled", icon: "", disabled: true },
{ label: "Also enabled", icon: "", disabled: false },
];
selected: 0;
variant: TabsVariant.pill;
Text {
text: "The middle tab cannot be selected.";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
}
}// `disabled(true)` drops the tab's message, which is what makes it inert
// without touching the selection.
let disabled = Tabs::new([
Tab::new("Enabled").on_press(Message::Choose("disabled", 0)),
Tab::new("Disabled")
.disabled(true)
.on_press(Message::Choose("disabled", 1)),
Tab::new("Also enabled").on_press(Message::Choose("disabled", 2)),
])
.variant(Variant::Pill)
.selected(app.disabled)
.body(muted_line("The middle tab cannot be selected."));#Dynamic Tabs
struct TabsView {
active_tab: usize,
tabs: Vec<String>,
}
impl Render for TabsView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
TabBar::new("dynamic-tabs")
.selected_index(self.active_tab)
.on_click(cx.listener(|view, index, _, cx| {
view.active_tab = *index;
cx.notify();
}))
.children(
self.tabs
.iter()
.map(|tab_name| Tab::new().label(tab_name.clone()))
)
}
}#Tabs with Menu
Use menu option to enable a dropdown menu for tab selection when there are many tabs,
this is default false.
If enable, the will have a dropdown button at the end of the tab bar to show all tabs in a menu.
TabBar::new("tabs-with-menu")
.menu(true)
.selected_index(0)
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile"))
.child(Tab::new().label("Documents"))
.child(Tab::new().label("Mail"))
.child(Tab::new().label("Settings"))#Maximum Tab Width
Use max_width to cap the width of each tab. For tabs created with .label(), text longer than the limit is truncated with an ellipsis automatically. Icon-only tabs (.icon()) are not affected. Prefix and suffix elements (e.g. a close button) are never truncated — the label yields space first. The more menu (when enabled) still shows the full label text.
Tabs built from custom children (via .child()) are only given the width limit; add truncate() yourself to the part that should shrink.
use gpui_kit::{div, px};
TabBar::new("tabs-with-max-width")
.max_width(px(100.))
.menu(true)
.selected_index(0)
.child(Tab::new().label("Account Settings & Preferences"))
.child(Tab::new().label("Documents & Files"))
.child(Tab::new().label("Appearance & Themes"))
.child(
Tab::new().child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Bot))
.child(div().truncate().child("Custom Child Tab")),
),
)#Scrollable Tabs
use gpui_kit::ScrollHandle;
struct ScrollableTabsView {
scroll_handle: ScrollHandle,
}
impl Render for ScrollableTabsView {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
TabBar::new("scrollable-tabs")
.track_scroll(&self.scroll_handle)
.child(Tab::new().label("Very Long Tab Name 1"))
.child(Tab::new().label("Very Long Tab Name 2"))
.child(Tab::new().label("Very Long Tab Name 3"))
.child(Tab::new().label("Very Long Tab Name 4"))
.child(Tab::new().label("Very Long Tab Name 5"))
}
}Tabs {
items: [
{ label: "Overview", icon: "", disabled: false },
{ label: "Activity", icon: "", disabled: false },
{ label: "Members", icon: "", disabled: false },
{ label: "Billing", icon: "", disabled: false },
{ label: "Integrations", icon: "", disabled: false },
{ label: "Security", icon: "", disabled: false },
{ label: "Audit log", icon: "", disabled: false },
];
selected: 0;
variant: TabsVariant.pill;
Text {
text: "Drag the bar sideways to reach the rest.";
color: Theme.muted-foreground;
font-size: Theme.font-size-sm;
}
}// Slint's bar flicks sideways when the labels do not fit. iced has no
// flickable, so the caller wraps the bar in `scrollable` for that; the bar
// itself is a plain row, and the demo names the difference rather than
// faking a drag.
let scrollable_tabs = Tabs::new([
Tab::new("Overview").on_press(Message::Choose("scrollable", 0)),
Tab::new("Activity").on_press(Message::Choose("scrollable", 1)),
Tab::new("Members").on_press(Message::Choose("scrollable", 2)),
Tab::new("Billing").on_press(Message::Choose("scrollable", 3)),
Tab::new("Integrations").on_press(Message::Choose("scrollable", 4)),
Tab::new("Security").on_press(Message::Choose("scrollable", 5)),
Tab::new("Audit log").on_press(Message::Choose("scrollable", 6)),
])
.variant(Variant::Pill)
.selected(app.scrollable)
.body(muted_line(
"iced's bar is a plain row; wrap it in `scrollable` to reach the rest.",
));#Individual Tab Configuration
TabBar::new("custom-tabs")
.child(
Tab::new().label("Custom Tab")
.id("custom-id")
.prefix(IconName::Star)
.suffix(IconName::X)
.on_click(|_, _, _| {
println!("Custom tab clicked");
})
)#Advanced Examples
#Custom Tab Content
Tab::empty()
.child(
h_flex()
.items_center()
.gap_2()
.child(IconName::Folder)
.child("Documents")
.child(
div()
.px_1()
.py_0p5()
.text_xs()
.bg(cx.theme().accent)
.text_color(cx.theme().accent_foreground)
.rounded(cx.theme().radius.half())
.child("12")
)
)#Tabs with State Management
struct TabsWithContent {
active_tab: usize,
tab_contents: Vec<String>,
}
impl TabsWithContent {
fn render_tab_content(&self, cx: &mut Context<Self>) -> impl IntoElement {
match self.active_tab {
0 => div().child("Account content"),
1 => div().child("Profile content"),
2 => div().child("Settings content"),
_ => div().child("Unknown content"),
}
}
}
impl Render for TabsWithContent {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.child(
TabBar::new("content-tabs")
.selected_index(self.active_tab)
.on_click(cx.listener(|view, index, _, cx| {
view.active_tab = *index;
cx.notify();
}))
.child(Tab::new().label("Account"))
.child(Tab::new().label("Profile"))
.child(Tab::new().label("Settings"))
)
.child(
div()
.flex_1()
.p_4()
.child(self.render_tab_content(cx))
)
}
}#Tabs with Close Buttons
While the basic Tab component doesn’t include closeable functionality, you can create closeable tabs using suffix elements:
struct CloseableTabsView {
tabs: Vec<String>,
active_tab: usize,
}
impl CloseableTabsView {
fn close_tab(&mut self, index: usize, cx: &mut Context<Self>) {
if self.tabs.len() > 1 {
self.tabs.remove(index);
if self.active_tab >= index && self.active_tab > 0 {
self.active_tab -= 1;
}
cx.notify();
}
}
}
impl Render for CloseableTabsView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
TabBar::new("closeable-tabs")
.selected_index(self.active_tab)
.on_click(cx.listener(|view, index, _, cx| {
view.active_tab = *index;
cx.notify();
}))
.children(
self.tabs
.iter()
.enumerate()
.map(|(index, tab_name)| {
Tab::new().label(tab_name.clone())
.suffix(
Button::new(format!("close-{}", index))
.icon(IconName::X)
.ghost()
.xsmall()
.on_click(cx.listener(move |view, _, _, cx| {
view.close_tab(index, cx);
}))
)
})
)
}
}#Notes
- The
TabBarmanages the selection state of all child tabs - Individual tab
on_clickhandlers are ignored whenTabBar.on_clickis set - Tabs automatically inherit the variant and size from their parent
TabBar - The
with_menuoption adds a dropdown for tab selection when there are many tabs - Scrolling is automatically enabled when tabs overflow the container width
- The dock system provides advanced closeable tab functionality for complex layouts
#API Reference
#TabBar
| Method | Description |
|---|---|
new(id) | Create a new tab bar with the given ID |
child(tab) | Add a tab to the bar |
children(tabs) | Add multiple tabs to the bar |
selected_index(index) | Set the active tab index |
on_click(fn) | Callback when a tab is clicked, receives tab index |
prefix(element) | Add element before the tabs |
suffix(element) | Add element after the tabs |
last_empty_space(element) | Custom element for empty space at the end |
track_scroll(handle) | Enable scrolling with a scroll handle |
with_menu(bool) | Enable dropdown menu for tab selection |
max_width(width) | Set maximum width of each tab; truncates |
#TabBar Variants
| Method | Description |
|---|---|
with_variant(variant) | Set the tab variant for all children |
underline() | Use underline variant |
pill() | Use pill variant |
outline() | Use outline variant |
segmented() | Use segmented variant |
#Tab
| Method | Description |
|---|---|
new(label) | Create a new tab with a label |
empty() | Create an empty tab |
icon(icon) | Create a tab with only an icon |
id(id) | Set custom ID for the tab |
with_variant(variant) | Set the tab variant |
pill() | Use pill variant |
outline() | Use outline variant |
segmented() | Use segmented variant |
underline() | Use underline variant |
prefix(element) | Add element before tab content |
suffix(element) | Add element after tab content |
disabled(bool) | Set disabled state |
selected(bool) | Set selected state (usually handled by TabBar) |
on_click(fn) | Custom click handler for individual tab |
#TabVariant
pub enum TabVariant {
Tab, // Default bordered tabs
Outline, // Rounded outline tabs
Pill, // Rounded pill-shaped tabs
Segmented, // Segmented control style
Underline, // Underline indicator tabs
}#Styling
Both TabBar and Tab implement Sizable trait:
xsmall()- Extra small sizesmall()- Small sizemedium()- Medium size (default)large()- Large size
#TabsVariant
underline, pill, outline
#TabItem
| Name | Kind | Type | Default |
|---|---|---|---|
label | field | string | |
icon | field | string | |
disabled | field | bool |
#Tabs
A tab bar plus content switch. Mirrors GPUI Component's `Tabs`.
| Name | Kind | Type | Default |
|---|---|---|---|
items | in | [TabItem] | |
selected | in-out | int | 0 |
variant | in | TabsVariant | TabsVariant.underline |
size | in | ControlSize | ControlSize.medium |
changed | callback | (index: int) |