Skip to content
PorabuildHeroGPUI component library
v0.14.0 · Apache-2.0

Navigation

Tabs

Organise content into switchable panels.

Rust
use herogpui::components::tabs::{TabItem, Tabs};

Tabs on docs.rsSource at v0.14.0

Usage

Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.

Usage

Tab labels use a 20px line height independently of surrounding text. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-usage",
        vec![
            TabItem::new("photos", "Photos")
                .content(gpui::div().child("Your photo library.")),
            TabItem::new("music", "Music")
                .content(gpui::div().child("Playlists and albums.")),
            TabItem::new("videos", "Videos")
                .content(gpui::div().child("Everything you have filmed.")),
        ],
        "photos",
    )
    .into_any_element()])

Label Size

`text_size` replaces the tab labels' size step; a 12/14/16px size takes v3's 16/20/24 leading. Each tab keeps its fixed height and padding. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-label-size",
        vec![
            TabItem::new("a", "Account")
                .content(gpui::div().child("12px labels.")),
            TabItem::new("b", "Billing")
                .content(gpui::div().child("12px labels.")),
        ],
        "a",
    )
    .text_size(px(12.))
    .into_any_element()])

Sizes

`size` is additive, not a prop: `Md` is the 32px box with 16px padding and a 14px label; `Sm` is 28/12/12 with 16px leading. The secondary underline keeps its thickness. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{TabItem, Tabs, TabsSize};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Tabs::new(
            "tabs-size-sm",
            vec![
                TabItem::new("a", "Account")
                    .content(gpui::div().child("Small step.")),
                TabItem::new("b", "Billing")
                    .content(gpui::div().child("Small step.")),
            ],
            "a",
        )
        .size(TabsSize::Sm)
        .into_any_element(),
        Tabs::new(
            "tabs-size-md",
            vec![
                TabItem::new("a", "Account")
                    .content(gpui::div().child("Medium step.")),
                TabItem::new("b", "Billing")
                    .content(gpui::div().child("Medium step.")),
            ],
            "a",
        )
        .size(TabsSize::Md)
        .into_any_element(),
    ])

Vertical

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Orientation, TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-vertical",
        vec![
            TabItem::new("account", "Account")
                .content(gpui::div().child("Name, email and password.")),
            TabItem::new("billing", "Billing")
                .content(gpui::div().child("Cards and invoices.")),
            TabItem::new("team", "Team")
                .content(gpui::div().child("Members and roles.")),
        ],
        "account",
    )
    .orientation(Orientation::Vertical)
    .into_any_element()])

Vertical Tab Width

`vertical_tab_min_width` replaces the 80px floor every tab of a vertical list keeps; a tab's own `width` still wins over it. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Orientation, TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-vertical-min-width",
        vec![
            TabItem::new("a", "A").content(gpui::div().child("A 48px rail.")),
            TabItem::new("b", "B").content(gpui::div().child("Second pane.")),
            TabItem::new("c", "C").content(gpui::div().child("Third pane.")),
        ],
        "a",
    )
    .orientation(Orientation::Vertical)
    .vertical_tab_min_width(px(48.))
    .into_any_element()])

Wrapping Labels

The label slot releases its min-content width, so long labels wrap inside constrained horizontal shares and vertical tab columns. Tabs keep the fixed 32px height and ship no truncation utility, so wrapped lines paint past the pill exactly as the v3.2.5 sheet renders them. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Orientation, TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        Tabs::new(
            "tabs-wrapping",
            vec![
                TabItem::new(
                    "profile",
                    "Profile and account security and recovery",
                )
                .content(gpui::div().child("Security settings.")),
                TabItem::new(
                    "notifications",
                    "Notification preferences and delivery",
                )
                .content(gpui::div().child("Notification settings.")),
                TabItem::new("billing", "Billing and invoices")
                    .content(gpui::div().child("Billing settings.")),
            ],
            "profile",
        )
        .full_width(true)
        .sx(|el| el.w(px(320.)))
        .into_any_element(),
        gpui::div()
            .w(px(320.))
            .h(px(180.))
            .child(
                Tabs::new(
                    "tabs-vertical-wrapping",
                    vec![
                        TabItem::new(
                            "profile",
                            "Profile and account security and recovery settings",
                        )
                        .content(gpui::div().child("Security settings.")),
                        TabItem::new("billing", "Billing and invoices")
                            .content(gpui::div().child("Billing settings.")),
                    ],
                    "profile",
                )
                .orientation(Orientation::Vertical)
                .into_any_element(),
            )
            .into_any_element(),
    ])

Full Width

Stretches a tab list with `<Tabs.List style="w-full">` and `<Tabs.Trigger style="flex-1">`; `full_width(true)` is that pair. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![gpui::div()
        .w(px(420.))
        .child(
            Tabs::new(
                "tabs-full-width",
                vec![
                    TabItem::new("all", "All")
                        .content(gpui::div().child("Everything.")),
                    TabItem::new("unread", "Unread")
                        .content(gpui::div().child("Only unread.")),
                    TabItem::new("archived", "Archived")
                        .content(gpui::div().child("Put away.")),
                ],
                "all",
            )
            .full_width(true),
        )
        .into_any_element()])

Alignment

Align content inside each tab to Start, Center (the default), or End. Selection and the indicator keep the same geometry. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Orientation, TabItem, Tabs, TabsAlign, TabsVariant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(TabsVariant::ALL.into_iter().map(|variant| {
        gpui::div()
            .flex()
            .flex_wrap()
            .items_center()
            .justify_center()
            .gap(px(12.))
            .children(TabsAlign::ALL.into_iter().map(|align| {
                        Tabs::new(
                            format!("tabs-align-{variant:?}-{align:?}"),
                            vec![
                                TabItem::new("general", "General"),
                                TabItem::new("billing", "Subscription & Billing"),
                                TabItem::new("privacy", "Privacy"),
                            ],
                            "general",
                        )
                        .variant(variant)
                        .orientation(Orientation::Vertical)
                        .align(align)
                        .sx(|el| el.h(px(116.)))
                            .into_any_element()
                    }))
            .into_any_element()
    }))

Overflow

More tabs than fit scroll along their axis. Wheel input from the other axis continues to the page. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, Orientation, TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        para("Horizontal", cx),
        // The list only overflows inside a bounded box, which is
        // how v3's own example frames it.
        gpui::div()
            .w(px(420.))
            .child(Tabs::new(
                "tabs-overflow",
                (1..=12)
                    .map(|n| {
                        TabItem::new(
                            SharedString::from(format!("t{n}")),
                            SharedString::from(format!("Section {n}")),
                        )
                        .content(gpui::div().child(format!("Content {n}")))
                    })
                    .collect(),
                "t1",
            ))
            .into_any_element(),
        para("Vertical", cx),
        gpui::div()
            .h(px(200.))
            .child(
                Tabs::new(
                    "tabs-overflow-vertical",
                    (1..=8)
                        .map(|n| {
                            TabItem::new(
                                SharedString::from(format!("v{n}")),
                                SharedString::from(format!("Section {n}")),
                            )
                            .content(gpui::div().child(format!("Content {n}")))
                        })
                        .collect(),
                    "v1",
                )
                .orientation(Orientation::Vertical),
            )
            .into_any_element(),
    ])

Also calls para (included when you copy).

Disabled Tab

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-disabled",
        vec![
            TabItem::new("open", "Open")
                .content(gpui::div().child("Open items.")),
            // v3: `<Tabs.Tab isDisabled>` on a single tab; the
            // selected "open" tab stays live.
            TabItem::new("closed", "Closed")
                .is_disabled(true)
                .content(gpui::div().child("Closed items.")),
        ],
        "open",
    )
    .into_any_element()])

With Separator

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-separator",
        vec![
            TabItem::new("one", "One").content(gpui::div().child("First.")),
            // v3: `<Tabs.Separator />` inside every tab but
            // the first.
            TabItem::new("two", "Two")
                .separator()
                .content(gpui::div().child("Second.")),
            TabItem::new("three", "Three")
                .separator()
                .content(gpui::div().child("Third.")),
        ],
        "one",
    )
    .into_any_element(),])

Icon-Only Segmented Control

The capture-toolbar shape: `trigger` renders an element instead of the label text — `label` stays the accessible name — and `radius`, `list_bg`, `indicator_bg` and `indicator_shadow` restyle the tray and its pill. The icon carries no accessible name of its own and svg does not inherit the tab's text colour, so it is set explicitly. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, TabItem, Tabs, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-icon-segmented",
        vec![
            TabItem::new("light", "Light").trigger(
                gpui::svg()
                    .size(px(16.))
                    .path(icons::SUN)
                    .text_color(cx.colors().foreground),
            ),
            TabItem::new("dark", "Dark").trigger(
                gpui::svg()
                    .size(px(16.))
                    .path(icons::MOON)
                    .text_color(cx.colors().foreground),
            ),
            TabItem::new("system", "System").trigger(
                gpui::svg()
                    .size(px(16.))
                    .path(icons::GEAR)
                    .text_color(cx.colors().foreground),
            ),
        ],
        "light",
    )
    .radius(px(24.))
    .list_bg(cx.colors().muted.alpha(0.10))
    .indicator_bg(cx.colors().muted.alpha(0.25))
    .indicator_shadow(false)
    .into_any_element()])

Icon-Only Toolbar Segments

The fixed-box toolbar shape: `width`, `height` and `padding_x` fix every icon segment at 32px with no side padding — spells that `TabsTrigger style="size-8 p-0"` — while `list_padding(0)` removes the primary tray's inset the way `Tabs.List style="p-0"` does, and `hover_fill(false)` drops the unselected-tab wash so the pointer only changes the label colour, not the fill. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{ActiveTheme, TabItem, Tabs, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-icon-toolbar",
        vec![
            TabItem::new("preview", "Preview")
                .width(px(32.))
                .height(px(32.))
                .padding_x(px(0.))
                .trigger(
                    gpui::svg()
                        .size(px(16.))
                        .path(icons::EYE)
                        .text_color(cx.colors().foreground),
                ),
            TabItem::new("duplicate", "Duplicate")
                .width(px(32.))
                .height(px(32.))
                .padding_x(px(0.))
                .trigger(
                    gpui::svg()
                        .size(px(16.))
                        .path(icons::COPY)
                        .text_color(cx.colors().foreground),
                ),
            TabItem::new("discard", "Discard")
                .width(px(32.))
                .height(px(32.))
                .padding_x(px(0.))
                .trigger(
                    gpui::svg()
                        .size(px(16.))
                        .path(icons::TRASH)
                        .text_color(cx.colors().foreground),
                ),
        ],
        "preview",
    )
    .list_padding(px(0.))
    .hover_fill(false)
    .into_any_element()])

Secondary Variant

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{TabItem, Tabs, TabsVariant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

let secondary = self.tab_underline.clone();
let items = || {
    vec![
        TabItem::new("home", "Home").content(gpui::div().child("The home panel.")),
        TabItem::new("music", "Music").content(gpui::div().child("The music panel.")),
        TabItem::new("videos", "Videos").content(gpui::div().child("The videos panel.")),
    ]
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-secondary",
        items(),
        secondary.clone(),
    )
    .selected_key(secondary)
    .variant(TabsVariant::Secondary)
    .on_selection_change(cx.listener(|this, key: &SharedString, _, cx| {
        this.tab_underline = key.clone();
        cx.notify();
    }))
    .into_any_element()])

Secondary Variant Vertical

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Orientation, TabItem, Tabs, TabsVariant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let items = || {
    vec![
        TabItem::new("home", "Home").content(gpui::div().child("The home panel.")),
        TabItem::new("music", "Music").content(gpui::div().child("The music panel.")),
        TabItem::new("videos", "Videos").content(gpui::div().child("The videos panel.")),
    ]
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new(
        "tabs-secondary-vertical",
        items(),
        "home",
    )
    .variant(TabsVariant::Secondary)
    .orientation(Orientation::Vertical)
    .into_any_element()])

Primary

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

let primary = self.tab_solid.clone();
let items = || {
    vec![
        TabItem::new("home", "Home").content(gpui::div().child("The home panel.")),
        TabItem::new("music", "Music").content(gpui::div().child("The music panel.")),
        TabItem::new("videos", "Videos").content(gpui::div().child("The videos panel.")),
    ]
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![Tabs::new("tabs-primary", items(), primary.clone())
        .selected_key(primary)
        .on_selection_change(cx.listener(|this, key: &SharedString, _, cx| {
            this.tab_solid = key.clone();
            cx.notify();
        }))
        .into_any_element()])

Sidebar

HeroGPUI extension, not a example: `Sidebar` is an application sidebar with a header, groups of menu items (icon, label, badge) and a footer. The menu is one tab stop: Up and Down move over the items and the Projects heading (skipping the disabled Archive), Right and Left expand and collapse a group, Enter selects. The footer button collapses it to icons, each named by a tooltip. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, IconName, Sidebar, SidebarGroup, SidebarItem, TabItem, Tabs};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        gpui::div()
            .h(px(420.))
            .w(px(560.))
            .flex()
            .rounded(px(12.))
            .border_1()
            .border_color(cx.colors().border)
            .overflow_hidden()
            .child(
                Sidebar::new(
                    "tabs-sidebar-nav",
                    vec![
                        SidebarGroup::new("main").items(vec![
                            SidebarItem::new("home", "Home").icon(IconName::House),
                            SidebarItem::new("inbox", "Inbox").icon(IconName::Inbox).badge("12"),
                            SidebarItem::new("calendar", "Calendar").icon(IconName::Calendar),
                            SidebarItem::new("archive", "Archive").icon(IconName::Archive).is_disabled(true),
                        ]),
                        SidebarGroup::new("projects")
                            .label("Projects")
                            .is_collapsible(true)
                            .items(vec![
                                SidebarItem::new("website", "Website").icon(IconName::Globe),
                                SidebarItem::new("dashboard", "Dashboard").icon(IconName::LayoutDashboard),
                            ]),
                        SidebarGroup::new("account").label("Account").items(vec![
                            SidebarItem::new("team", "Team").icon(IconName::Users),
                            SidebarItem::new("settings", "Settings").icon(IconName::Settings),
                        ]),
                    ],
                )
                .width(px(220.))
                .header(
                    gpui::div()
                        .px(px(8.))
                        .py(px(4.))
                        .font_weight(gpui::FontWeight::SEMIBOLD)
                        .whitespace_nowrap()
                        .child(if self.sidebar_collapsed { "A" } else { "Acme Inc." }),
                )
                .active_key(Some(self.sidebar_active.clone()))
                .on_select(cx.listener(|this, key: &SharedString, _, cx| {
                    this.sidebar_active = key.clone();
                    cx.notify();
                }))
                .is_collapsed(self.sidebar_collapsed)
                .on_collapsed_change(cx.listener(|this, collapsed: &bool, _, cx| {
                    this.sidebar_collapsed = *collapsed;
                    cx.notify();
                })),
            )
            .child(
                gpui::div()
                    .flex_1()
                    .p(px(20.))
                    .text_color(cx.colors().muted)
                    .child(format!("Page: {}", self.sidebar_active)),
            )
            .into_any_element(),
    ])

Helpers used by these examples

Plain functions over the public API, defined once here and copied with each example that calls them.

para
fn para(text: &str, cx: &App) -> gpui::AnyElement {
    let colors = cx.colors();
    gpui::div()
        .text_size(px(14.5))
        .line_height(px(24.))
        .text_color(colors.foreground)
        .max_w(px(720.))
        .child(text.to_owned())
        .into_any_element()
}

Anatomy

Rust types

TabsTabItem

Tabs is assembled from these builders. The API reference lists each one.

Customization

Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Tabs looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.

Styling

Tabs styling
MethodValuesDescription
variant(TabsVariant)TabsVariant::Primary | TabsVariant::SecondaryVisual style variant: a filled segment or an underline.
orientation(Orientation)Orientation::Horizontal | Orientation::VerticalTab-list and panel layout orientation.
Rust
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
    el.bg(gpui::rgba(0xffa500ff))             // background
        .text_color(gpui::rgba(0x000000ff))   // text
        .w(gpui::px(13.))                     // size
        .h(gpui::px(12.))
})

API reference

Against HeroUI v3.2.6: 21 entries ported, 1 partial, 10 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.

Builders

Tabs builders
BuilderValuesDefaultDescription
align(TabsAlign)TabsAlign::Start | TabsAlign::Center | TabsAlign::EndTabsAlign::CenterAligns content inside each tab, independently of the panel and nested tab lists. Start/end use the port's left-to-right layout.
variant(TabsVariant)TabsVariant::Primary | TabsVariant::SecondaryTabsVariant::PrimaryVisual style variant: a filled segment or an underline.
orientation(Orientation)Orientation::Horizontal | Orientation::VerticalOrientation::HorizontalTab-list and panel layout orientation.
selected_key(key)string—Controlled selected tab key.
default_selected_key(key)string—Initial selected key for uncontrolled tabs.
on_selection_change(callback)Fn(key: Key) -> ()—Reports the selected key from tab activation.
new(key, label)string—Unique tab identifier and selection key.
is_disabled(bool)true | falsefalseRemoves one tab from pointer and roving-keyboard activation.
Not ported10 HeroUI entries not ported
Tabs builders not ported
HeroUI v3Why it is not ported
Tabs.classNameOn the root.
Tabs.renderOverrides the root element.
Tabs.List.aria-labelAccessible label for the tab list.
Tabs.List.classNameOn the tab list.
Tabs.List.renderOverrides the tab-list element.
Tabs.Tab.classNameOn one tab.
Tabs.Tab.renderOverrides one tab's element.
Tabs.Separator.classNameOn a separator.
Tabs.Panel.classNameOn a panel.
Tabs.Panel.renderOverrides the panel element.

Parts

Tabs parts
PartDescription
TabsRoot flex container that orients the list and active panel.
TabsContainer owning the scroller and overflow chevrons.
TabsScrollable row or column and its shared roving focus handle.
TabItemOne keyed, selectable tab segment; `TabItem::trigger` replaces the label text with an icon element while `label` stays the accessible name.
TabItemOptional pointer-inert hairline before a primary tab.
TabsMeasured filled pill or accent line following the selected tab.
TabItemContent associated with the selected tab key.

States

Tabs states
StateBuilderDescription
Selectedselected_keyMoves the indicator and applies selected foreground styling.
Hoveredhover(.. opacity(0.7))Dims an enabled, unselected tab to 70% opacity.
Focus visiblewith_focus_ringDraws the shared roving stop's focus-visible ring.
Disabledis_disabled || item.is_disabledDims the tab and removes it from pointer and keyboard activation.
Controlled selectionselected_keyMoves roving focus and reports changes without mutating owner state.
Uncontrolled selectiondefault_selected_keyHolds selection and repairs it when the selected item disappears.