# Tabs

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

```slint
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

```slint
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.

```slint
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.

```slint
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.

```slint
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.

```slint
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-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.
