Tabs
A segmented strip that owns the selection and leaves the panel to you.
import { Tabs } from "@glint/components/tabs.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 620px; height: 240px; background: Tokens.color-background;
in-out property <int> active-tab: 0;
VerticalLayout { padding: 24px; spacing: 20px; alignment: start;
HorizontalLayout { alignment: start;
Tabs { accessible-label: "Account settings"; items: [ { label: "Account" }, { label: "Password" }, { label: "Sessions" }, ]; current <=> root.active-tab; } }
if root.active-tab == 0: Text { text: "Make changes to your account here."; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-size; } if root.active-tab == 1: Text { text: "Change your password. You will be signed out elsewhere."; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-size; } if root.active-tab == 2: Text { text: "Three devices are signed in to this account."; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-size; } }}Usage
import { Tabs } from "@glint/components/tabs.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component AppWindow inherits Window { width: 520px; height: 200px; background: Tokens.color-background;
in-out property <int> active-tab: 0;
VerticalLayout { padding: 24px; spacing: 16px; alignment: start;
// Size the strip to its content rather than to the window. HorizontalLayout { alignment: start;
Tabs { accessible-label: "Sections"; items: [{ label: "Account" }, { label: "Password" }]; current <=> root.active-tab; changed(index) => { debug("tab", index); } } }
if active-tab == 0: VerticalLayout { Text { text: "Account"; } } if active-tab == 1: VerticalLayout { Text { text: "Password"; } } }}Tabs is the strip, not the tab set. Slint gives a component one @children
slot, and a strip that also held panels would need one per tab — so the panels
are yours, driven from current. That is the whole integration: bind current
two-way, and put an if per tab under the strip.
current is two-way and changed fires with the new index, so a tab set can
be driven from outside as well as clicked.
A strip placed straight into a layout stretches across it; put it in a
HorizontalLayout { alignment: start; } to size it to its own triggers, as
every example here does.
Examples
Variants
variant is how the strip is drawn. TabsVariant.default is the segmented
control: a muted pill holding the triggers, with the active one lifted onto a
background surface. TabsVariant.line is the underline strip: no container at
all, the active trigger marked by a rule along the edge the strip runs against.
import { Tabs, TabsVariant } from "@glint/components/tabs.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 620px; height: 240px; background: Tokens.color-background;
in-out property <int> segmented: 0; in-out property <int> underlined: 1;
VerticalLayout { padding: 24px; spacing: 28px; alignment: center;
HorizontalLayout { alignment: start; Tabs { accessible-label: "Segmented"; variant: TabsVariant.default; items: [{ label: "Preview" }, { label: "Code" }, { label: "Notes" }]; current <=> root.segmented; } }
HorizontalLayout { alignment: start; Tabs { accessible-label: "Underlined"; variant: TabsVariant.line; items: [{ label: "Preview" }, { label: "Code" }, { label: "Notes" }]; current <=> root.underlined; } } }}Standing the strip on its side
orientation turns the strip into a column — the rail a settings page wears
down its left edge. The arrows follow the axis with it: ← and → along a row,
↑ and ↓ down a column. The line variant moves its rule from under the active
trigger to beside it.
import { Tabs, TabsOrientation, TabsVariant } from "@glint/components/tabs.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 620px; height: 260px; background: Tokens.color-background;
in-out property <int> section: 0;
HorizontalLayout { padding: 24px; spacing: 24px; alignment: start;
Tabs { accessible-label: "Settings"; orientation: TabsOrientation.vertical; variant: TabsVariant.line; items: [ { label: "Profile", icon: IconSet.User }, { label: "Notifications", icon: IconSet.Bell }, { label: "Billing", icon: IconSet.CreditCard }, ]; current <=> root.section; }
VerticalLayout { alignment: start; Text { text: root.section == 0 ? "Your name, avatar and handle." : root.section == 1 ? "What we email you about." : "Plan, invoices and payment method."; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-size; wrap: word-wrap; } } }}Icons in the triggers
A TabItem carries a lucide icon beside its label. An empty one takes no
column, so a strip mixing labelled and iconless tabs stays even.
import { Tabs } from "@glint/components/tabs.slint";import { Tokens } from "@glint/theme/tokens.slint";import { IconSet } from "@lucide";
export component Demo inherits Window { width: 620px; height: 180px; background: Tokens.color-background;
in-out property <int> view: 0;
VerticalLayout { padding: 24px; alignment: center;
HorizontalLayout { alignment: center;
Tabs { accessible-label: "View"; items: [ { label: "Grid", icon: IconSet.LayoutGrid }, { label: "List", icon: IconSet.List }, { label: "Board", icon: IconSet.Columns3 }, ]; current <=> root.view; } } }}A tab the app has taken away
disabled on an item is a section this account cannot open. It keeps its place
in the strip rather than being dropped from items — dropping it moves every
tab beside it — dims, announces itself unavailable, and refuses every
activation path. The arrows step over it, however many stand in a row.
A current pointing at a disabled tab leaves the strip with nothing drawn as
chosen, rather than drawing that tab as the choice and announcing it as
selected and unavailable.
import { Tabs } from "@glint/components/tabs.slint";import { Tokens } from "@glint/theme/tokens.slint";
export component Demo inherits Window { width: 620px; height: 200px; background: Tokens.color-background;
in-out property <int> tab: 0;
VerticalLayout { padding: 24px; spacing: 16px; alignment: center;
HorizontalLayout { alignment: center;
Tabs { accessible-label: "Workspace"; items: [ { label: "Overview" }, { label: "Members", disabled: true }, { label: "Audit log", disabled: true }, { label: "Danger zone" }, ]; current <=> root.tab; } }
Text { text: "Tab " + root.tab + " is active. The arrows skip the two " + "disabled sections between them."; color: Tokens.color-muted-foreground; font-size: Tokens.typography-body-sm-size; horizontal-alignment: center; wrap: word-wrap; } }}API Reference
Properties
| Property | Type | Default | Description |
|---|---|---|---|
items | in [TabItem] | no default | Tab definitions in display order. |
current | in-out int | 0 | Two-way; index of the active tab. |
variant | in TabsVariant | TabsVariant.default | Style — default (segmented) or line (underlined). |
orientation | in TabsOrientation | TabsOrientation.horizontal | horizontal (default) or vertical. |
Callbacks
| Callback | Description |
|---|---|
changed(int) | Fired with the new current index. |
Enums
| Enum | Values |
|---|---|
TabsVariant | default, line |
TabsOrientation | horizontal, vertical |
TabItem is a data type rather than a component. It carries label, a lucide
icon, and disabled — the tab that keeps its place and refuses.
Accessibility
- A tab list of tabs. The strip carries
accessible-role: tab-listwith its item count and the axis its arrows walk (accessible-orientation); each trigger is anaccessible-role: tabcarrying its label, its index, its selected state and a default action. Naming the list is the call site’s — setaccessible-label. - One tab stop, and a delegated announcement. The strip is a single stop,
so it delegates the accessible focus to
current. Without that a screen reader announces the list rather than the tab the arrows landed on — the same move Slint’s ownTabWidgetmakes. - Selection follows the arrows. ← and → (↑ and ↓ when vertical) move the active tab and wrap at both ends; Enter and Space re-activate the tab they landed on rather than moving anywhere. Arrows off the strip’s axis are left to the app.
- A disabled tab announces itself unavailable. It reports
accessible-enabled: false, keeps its index in the count, and refuses the pointer, the keyboard and the accessible default action alike — one guard, so the three cannot drift apart. - The panels are outside this component, so their announcement is yours.
Slint has a
tab-panelrole to put on the region a tab reveals, but no way to point a tab at it — there is noaccessible-controls— so the association a reader gets is the reading order. Put the panel directly after the strip. - The focus ring is keyboard-only, and draws around the whole strip rather than around the tab inside it.