Tabs
<!-- curated-component-guide -->
This demo runs the real GPUI component on WebGPU, which this browser does not expose.
Try Chrome or Edge 113+, or Safari 26+. In Firefox, enable dom.webgpu.enabled.
You can also run it natively: cargo run -p showcase tabs
The demo failed to start in this browser.
You can run it natively instead: cargo run -p showcase tabs
Live examples are not supported on most mobile browsers yet — open this page on a desktop or tablet to run the demo.
Recent phones with WebGPU may manage it (downloads ~18 MB):
#Tabs
Panels of content shown one at a time, selected by tab.
#Anatomy
Import the parts and compose them under TabsRoot:
use base_gpui::tabs::{
TabsIndicator, TabsList, TabsPanel, TabsRoot, TabsTab,
};
TabsRoot::new()
.default_value(Some("overview"))
.child(
TabsList::new()
.child(
TabsTab::new()
.value("overview")
.child("Overview"),
)
.child(
TabsTab::new()
.value("settings")
.child("Settings"),
)
.child(TabsIndicator::new()),
)
.child(
TabsPanel::new()
.value("overview")
.id("overview-panel")
.child("Overview content"),
)
.child(
TabsPanel::new()
.value("settings")
.id("settings-panel")
.child("Settings content"),
)
Every TabsTab and its matching TabsPanel must use the same value. TabsIndicator belongs inside TabsList so it can use the measured tab positions.
#TabsRoot
The top-level container. It owns the selected value, coordinates all parts, and defines the orientation.
use base_gpui::tabs::{TabsOrientation, TabsRoot};
TabsRoot::new()
.id("account-tabs")
.default_value(Some("profile"))
.orientation(TabsOrientation::Horizontal)
.on_value_change(|value, window, cx| {
// `value` is the newly selected `Option<&T>`.
})
.style_with_state(|state, root| {
// `state.orientation`
// `state.activation_direction`
root
})
.child(/* TabsList or TabsPanel */)
#Builders
| Builder | Signature & description |
|---|---|
.id(id) | pub fn id(self, id: impl Into<ElementId>) -> SelfSets the stable identity used to scope the tabs' runtime state. The default is |
.value(value) | pub fn value(self, value: Option<T>) -> SelfControls the selected tab. Passing this builder makes the root controlled: user interaction calls |
.default_value(default_value) | pub fn default_value(self, default_value: Option<T>) -> SelfSets the initial selected tab for an uncontrolled root. Later changes to Use either |
.on_value_change(on_value_change) | pub fn on_value_change( self, on_value_change: impl Fn(Option<&T>, &mut Window, &mut App) + 'static, ) -> SelfRuns when selection is requested. The first argument is the next selected value, or |
.orientation(orientation) | pub fn orientation(self, orientation: TabsOrientation) -> SelfSets |
.child(child) / .children(children) | pub fn child(self, child: impl Into<TabsChild<T>>) -> Self pub fn children( self, children: impl IntoIterator<Item = impl Into<TabsChild<T>>>, ) -> SelfAdds a |
.style_with_state(style) | pub fn style_with_state( self, style: impl Fn(TabsRootStyleState, Div) -> Div + 'static, ) -> SelfStyles the root using its |
#TabsList
Contains the tabs and optional indicator. It owns keyboard navigation and exposes the tab-list accessibility role.
use base_gpui::tabs::{TabsIndicator, TabsList, TabsTab};
TabsList::new()
.activate_on_focus(false)
.loop_focus(true)
.aria_label("Account sections")
.style_with_state(|state, list| {
// `state.orientation`
// `state.activation_direction`
list
})
.child(TabsTab::new().value("profile").child("Profile"))
.child(TabsIndicator::new())
#Builders
| Builder | Signature & description |
|---|---|
.activate_on_focus(activate_on_focus) | pub fn activate_on_focus(self, activate_on_focus: bool) -> SelfWhen |
.loop_focus(loop_focus) | pub fn loop_focus(self, loop_focus: bool) -> SelfControls whether keyboard navigation wraps from the final tab to the first tab and vice versa. The default is |
.aria_label(aria_label) | pub fn aria_label(self, aria_label: impl Into<SharedString>) -> SelfProvides an accessible name for the tab list. Use it when the list has no accessible visible heading. GPUI does not yet expose |
.child(child) / .children(children) | pub fn child(self, child: impl Into<TabsListChild<T>>) -> Self pub fn children( self, children: impl IntoIterator<Item = impl Into<TabsListChild<T>>>, ) -> SelfAdds a |
.style_with_state(style) | pub fn style_with_state( self, style: impl Fn(TabsListStyleState, Div) -> Div + 'static, ) -> SelfStyles the list using its |
#TabsTab
An interactive tab. It participates in roving focus, selects its matching panel, and exposes tab accessibility semantics.
use base_gpui::tabs::TabsTab;
TabsTab::new()
.id("profile-tab")
.value("profile")
.disabled(false)
.aria_label("Profile")
.style_with_state(|state, tab| {
// `state.active`
// `state.disabled`
// `state.highlighted`
// `state.orientation`
tab
})
.child("Profile")
#Builders
| Builder | Signature & description |
|---|---|
.value(value) | pub fn value(self, value: T) -> SelfAssociates the tab with a selection value. Use the same value on the corresponding |
.id(id) | pub fn id(self, id: impl Into<ElementId>) -> SelfSets the tab's stable element identity. The default is |
.disabled(disabled) | pub fn disabled(self, disabled: bool) -> SelfPrevents selection and removes the tab from keyboard navigation when |
.index(index) | pub fn index(self, index: usize) -> SelfExplicitly sets the tab's zero-based position. Normal compound composition assigns indices automatically, so application code generally should not call this builder. |
.aria_label(aria_label) | pub fn aria_label(self, aria_label: impl Into<SharedString>) -> SelfOverrides the tab's accessible name. This is primarily useful for icon-only tabs or when the accessible label should differ from visible content. |
.style_with_state(style) | pub fn style_with_state( self, style: impl Fn(TabsTabStyleState, Div) -> Div + 'static, ) -> SelfStyles the tab using
|
#TabsPanel
Contains the content associated with one tab. Inactive panels are unmounted by default.
use base_gpui::tabs::TabsPanel;
TabsPanel::new()
.id("profile-panel")
.value("profile")
.keep_mounted(false)
.style_with_state(|state, panel| {
// `state.hidden`
// `state.orientation`
// `state.activation_direction`
panel
})
.child("Profile content")
#Builders
| Builder | Signature & description |
|---|---|
.value(value) | pub fn value(self, value: T) -> SelfAssociates the panel with a tab value. The panel is active when this value equals the root's selected value. |
.id(id) | pub fn id(self, id: impl Into<ElementId>) -> SelfSets the panel's stable element ID. An ID is required for the active panel to enter the accessibility tree with |
.keep_mounted(keep_mounted) | pub fn keep_mounted(self, keep_mounted: bool) -> SelfKeeps inactive panel content mounted but invisible when |
.style_with_state(style) | pub fn style_with_state( self, style: impl Fn(TabsPanelStyleState, Div) -> Div + 'static, ) -> SelfStyles the panel using
|
#TabsIndicator
An optional visual marker for the selected tab. It renders only after the selected tab's bounds have been measured.
use base_gpui::tabs::TabsIndicator;
TabsIndicator::new()
.style_with_state(|state, indicator| {
// `state.selected`
// `state.active_tab_position`
// `state.active_tab_size`
// `state.orientation`
// `state.activation_direction`
indicator
})
#Builders
| Builder | Signature & description |
|---|---|
.style_with_state(style) | pub fn style_with_state( self, style: impl Fn(TabsIndicatorStyleState, Div) -> Div + 'static, ) -> SelfStyles and positions the indicator from the selected tab's measured position and size.
|
#Keyboard interaction
| Key | Behavior |
|---|---|
| Left / Right | Moves between tabs in horizontal orientation |
| Up / Down | Moves between tabs in vertical orientation |
| Home | Moves to the first enabled tab |
| End | Moves to the last enabled tab |
| Enter / Space | Selects the focused tab when activate_on_focus is false |
Disabled tabs are skipped. Navigation wraps when loop_focus is enabled.
#Accessibility
TabsList exposes Role::TabList, each TabsTab exposes Role::Tab and selected state, and the active TabsPanel exposes Role::TabPanel. Roving focus ensures one enabled tab is in the tab order.
The pinned GPUI revision does not expose ID-reference relationship builders such as aria-controls or aria-labelledby. Use literal aria_label values where provided; these limitations are not silently emulated.