Skip to content
Glint UI

Tabs

A segmented strip that owns the selection and leaves the panel to you.

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.

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.

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.

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.

API Reference

Properties

PropertyTypeDefaultDescription
itemsin [TabItem]no defaultTab definitions in display order.
currentin-out int0Two-way; index of the active tab.
variantin TabsVariantTabsVariant.defaultStyle — default (segmented) or line (underlined).
orientationin TabsOrientationTabsOrientation.horizontalhorizontal (default) or vertical.

Callbacks

CallbackDescription
changed(int)Fired with the new current index.

Enums

EnumValues
TabsVariantdefault, line
TabsOrientationhorizontal, 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-list with its item count and the axis its arrows walk (accessible-orientation); each trigger is an accessible-role: tab carrying its label, its index, its selected state and a default action. Naming the list is the call site’s — set accessible-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 own TabWidget makes.
  • 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-panel role to put on the region a tab reveals, but no way to point a tab at it — there is no accessible-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.