Tabs
Tablist primitive that synchronizes tab focus, selection, and panel visibility.
Default horizontal
With orientation omitted, Tabs arranges its tablist horizontally.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-tabs/ui-tabs.html"
>
<ui-tabs
label="View"
>
<section
id="preview"
aria-label="Preview"
>A live component preview.</section>
<section
id="code"
aria-label="Code"
>Semantic HTML.</section>
</ui-tabs><script setup>
import { UiTabs } from "@threadlabs/looma";
</script>
<template>
<UiTabs
label="View"
>
<section
id="preview"
aria-label="Preview"
>A live component preview.</section>
<section
id="code"
aria-label="Code"
>Semantic HTML.</section>
</UiTabs>
</template>import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="View"
>
<section
id="preview"
aria-label="Preview"
>A live component preview.</section>
<section
id="code"
aria-label="Code"
>Semantic HTML.</section>
</UiTabs>;
}/** @jsxImportSource preact */
import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="View"
>
<section
id="preview"
aria-label="Preview"
>A live component preview.</section>
<section
id="code"
aria-label="Code"
>Semantic HTML.</section>
</UiTabs>;
}<script>
import { UiTabs } from "@threadlabs/looma";
</script>
<UiTabs
label="View"
>
<section
id="preview"
aria-label="Preview"
>A live component preview.</section>
<section
id="code"
aria-label="Code"
>Semantic HTML.</section>
</UiTabs>import { Component } from "@angular/core";
import { UiTabs } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiTabs],
template: `
<div
ui-tabs
[label]="'View'"
>
<section
id="preview"
aria-label="Preview"
>A live component preview.</section>
<section
id="code"
aria-label="Code"
>Semantic HTML.</section>
</div>
`,
})
export default class Example {}stretch
Stretch shares the tab list's full width equally between tabs; by default each tab is only as wide as its label.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-tabs/ui-tabs.html"
>
<ui-tabs
label="Plan"
stretch
>
<section
id="monthly"
aria-label="Monthly"
>Billed every month.</section>
<section
id="yearly"
aria-label="Yearly"
>Billed once a year.</section>
<section
id="lifetime"
aria-label="Lifetime"
>One payment.</section>
</ui-tabs><script setup>
import { UiTabs } from "@threadlabs/looma";
</script>
<template>
<UiTabs
label="Plan"
:stretch="true"
>
<section
id="monthly"
aria-label="Monthly"
>Billed every month.</section>
<section
id="yearly"
aria-label="Yearly"
>Billed once a year.</section>
<section
id="lifetime"
aria-label="Lifetime"
>One payment.</section>
</UiTabs>
</template>import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="Plan"
stretch
>
<section
id="monthly"
aria-label="Monthly"
>Billed every month.</section>
<section
id="yearly"
aria-label="Yearly"
>Billed once a year.</section>
<section
id="lifetime"
aria-label="Lifetime"
>One payment.</section>
</UiTabs>;
}/** @jsxImportSource preact */
import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="Plan"
stretch
>
<section
id="monthly"
aria-label="Monthly"
>Billed every month.</section>
<section
id="yearly"
aria-label="Yearly"
>Billed once a year.</section>
<section
id="lifetime"
aria-label="Lifetime"
>One payment.</section>
</UiTabs>;
}<script>
import { UiTabs } from "@threadlabs/looma";
</script>
<UiTabs
label="Plan"
stretch
>
<section
id="monthly"
aria-label="Monthly"
>Billed every month.</section>
<section
id="yearly"
aria-label="Yearly"
>Billed once a year.</section>
<section
id="lifetime"
aria-label="Lifetime"
>One payment.</section>
</UiTabs>import { Component } from "@angular/core";
import { UiTabs } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiTabs],
template: `
<div
ui-tabs
[label]="'Plan'"
[stretch]="true"
>
<section
id="monthly"
aria-label="Monthly"
>Billed every month.</section>
<section
id="yearly"
aria-label="Yearly"
>Billed once a year.</section>
<section
id="lifetime"
aria-label="Lifetime"
>One payment.</section>
</div>
`,
})
export default class Example {}Overflow on narrow screens
When the tabs do not fit, the tab list scrolls sideways (swipe on touch screens) while the tab area stays full width.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-tabs/ui-tabs.html"
>
<div
style="max-width: 18rem"
>
<ui-tabs
label="Project"
>
<section
id="overview"
aria-label="Overview"
>Overview</section>
<section
id="activity"
aria-label="Activity"
>Activity</section>
<section
id="members"
aria-label="Members"
>Members</section>
<section
id="integrations"
aria-label="Integrations"
>Integrations</section>
<section
id="billing"
aria-label="Billing"
>Billing</section>
<section
id="settings-tab"
aria-label="Settings"
>Settings</section>
</ui-tabs>
</div><script setup>
import { UiTabs } from "@threadlabs/looma";
</script>
<template>
<div
style="max-width: 18rem"
>
<UiTabs
label="Project"
>
<section
id="overview"
aria-label="Overview"
>Overview</section>
<section
id="activity"
aria-label="Activity"
>Activity</section>
<section
id="members"
aria-label="Members"
>Members</section>
<section
id="integrations"
aria-label="Integrations"
>Integrations</section>
<section
id="billing"
aria-label="Billing"
>Billing</section>
<section
id="settings-tab"
aria-label="Settings"
>Settings</section>
</UiTabs>
</div>
</template>import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <div
style={{"maxWidth":"18rem"}}
>
<UiTabs
label="Project"
>
<section
id="overview"
aria-label="Overview"
>Overview</section>
<section
id="activity"
aria-label="Activity"
>Activity</section>
<section
id="members"
aria-label="Members"
>Members</section>
<section
id="integrations"
aria-label="Integrations"
>Integrations</section>
<section
id="billing"
aria-label="Billing"
>Billing</section>
<section
id="settings-tab"
aria-label="Settings"
>Settings</section>
</UiTabs>
</div>;
}/** @jsxImportSource preact */
import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <div
style={{"maxWidth":"18rem"}}
>
<UiTabs
label="Project"
>
<section
id="overview"
aria-label="Overview"
>Overview</section>
<section
id="activity"
aria-label="Activity"
>Activity</section>
<section
id="members"
aria-label="Members"
>Members</section>
<section
id="integrations"
aria-label="Integrations"
>Integrations</section>
<section
id="billing"
aria-label="Billing"
>Billing</section>
<section
id="settings-tab"
aria-label="Settings"
>Settings</section>
</UiTabs>
</div>;
}<script>
import { UiTabs } from "@threadlabs/looma";
</script>
<div
style="max-width: 18rem"
>
<UiTabs
label="Project"
>
<section
id="overview"
aria-label="Overview"
>Overview</section>
<section
id="activity"
aria-label="Activity"
>Activity</section>
<section
id="members"
aria-label="Members"
>Members</section>
<section
id="integrations"
aria-label="Integrations"
>Integrations</section>
<section
id="billing"
aria-label="Billing"
>Billing</section>
<section
id="settings-tab"
aria-label="Settings"
>Settings</section>
</UiTabs>
</div>import { Component } from "@angular/core";
import { UiTabs } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiTabs],
template: `
<div
style="max-width: 18rem"
>
<div
ui-tabs
[label]="'Project'"
>
<section
id="overview"
aria-label="Overview"
>Overview</section>
<section
id="activity"
aria-label="Activity"
>Activity</section>
<section
id="members"
aria-label="Members"
>Members</section>
<section
id="integrations"
aria-label="Integrations"
>Integrations</section>
<section
id="billing"
aria-label="Billing"
>Billing</section>
<section
id="settings-tab"
aria-label="Settings"
>Settings</section>
</div>
</div>
`,
})
export default class Example {}orientation="vertical"
Vertical orientation changes the tablist axis while preserving the same tab and panel relationships.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-tabs/ui-tabs.html"
>
<ui-tabs
label="Settings"
orientation="vertical"
value="profile"
>
<section
id="profile"
aria-label="Profile"
>Profile settings</section>
<section
id="security"
aria-label="Security"
>Security settings</section>
</ui-tabs><script setup>
import { UiTabs } from "@threadlabs/looma";
</script>
<template>
<UiTabs
label="Settings"
orientation="vertical"
value="profile"
>
<section
id="profile"
aria-label="Profile"
>Profile settings</section>
<section
id="security"
aria-label="Security"
>Security settings</section>
</UiTabs>
</template>import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="Settings"
orientation="vertical"
value="profile"
>
<section
id="profile"
aria-label="Profile"
>Profile settings</section>
<section
id="security"
aria-label="Security"
>Security settings</section>
</UiTabs>;
}/** @jsxImportSource preact */
import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="Settings"
orientation="vertical"
value="profile"
>
<section
id="profile"
aria-label="Profile"
>Profile settings</section>
<section
id="security"
aria-label="Security"
>Security settings</section>
</UiTabs>;
}<script>
import { UiTabs } from "@threadlabs/looma";
</script>
<UiTabs
label="Settings"
orientation="vertical"
value="profile"
>
<section
id="profile"
aria-label="Profile"
>Profile settings</section>
<section
id="security"
aria-label="Security"
>Security settings</section>
</UiTabs>import { Component } from "@angular/core";
import { UiTabs } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiTabs],
template: `
<div
ui-tabs
[label]="'Settings'"
[orientation]="'vertical'"
[value]="'profile'"
>
<section
id="profile"
aria-label="Profile"
>Profile settings</section>
<section
id="security"
aria-label="Security"
>Security settings</section>
</div>
`,
})
export default class Example {}Manual activation
Arrow keys browse tab labels without loading another panel; Enter or Space selects the focused tab.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-tabs/ui-tabs.html"
>
<ui-tabs
label="Reports"
activation="manual"
>
<section
id="summary-report"
aria-label="Summary"
>Summary report.</section>
<section
id="detail-report"
aria-label="Detail"
>Detail report.</section>
<section
id="history-report"
aria-label="History"
>History report.</section>
</ui-tabs><script setup>
import { UiTabs } from "@threadlabs/looma";
</script>
<template>
<UiTabs
label="Reports"
activation="manual"
>
<section
id="summary-report"
aria-label="Summary"
>Summary report.</section>
<section
id="detail-report"
aria-label="Detail"
>Detail report.</section>
<section
id="history-report"
aria-label="History"
>History report.</section>
</UiTabs>
</template>import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="Reports"
activation="manual"
>
<section
id="summary-report"
aria-label="Summary"
>Summary report.</section>
<section
id="detail-report"
aria-label="Detail"
>Detail report.</section>
<section
id="history-report"
aria-label="History"
>History report.</section>
</UiTabs>;
}/** @jsxImportSource preact */
import { UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="Reports"
activation="manual"
>
<section
id="summary-report"
aria-label="Summary"
>Summary report.</section>
<section
id="detail-report"
aria-label="Detail"
>Detail report.</section>
<section
id="history-report"
aria-label="History"
>History report.</section>
</UiTabs>;
}<script>
import { UiTabs } from "@threadlabs/looma";
</script>
<UiTabs
label="Reports"
activation="manual"
>
<section
id="summary-report"
aria-label="Summary"
>Summary report.</section>
<section
id="detail-report"
aria-label="Detail"
>Detail report.</section>
<section
id="history-report"
aria-label="History"
>History report.</section>
</UiTabs>import { Component } from "@angular/core";
import { UiTabs } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiTabs],
template: `
<div
ui-tabs
[label]="'Reports'"
[activation]="'manual'"
>
<section
id="summary-report"
aria-label="Summary"
>Summary report.</section>
<section
id="detail-report"
aria-label="Detail"
>Detail report.</section>
<section
id="history-report"
aria-label="History"
>History report.</section>
</div>
`,
})
export default class Example {}Rich tab labels
Author tab buttons in the tab slot when labels need icons or counts; each button value matches a panel id.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-tabs/ui-tabs.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-icon/ui-icon.html"
>
<ui-tabs
label="Workspace views"
>
<button
slot="tab"
value="inbox-view"
> <ui-icon
name="folder"
></ui-icon> Inbox <span>(3)</span> </button>
<button
slot="tab"
value="archive-view"
>Archive</button>
<section
id="inbox-view"
aria-label="Inbox"
>Unread and recent items.</section>
<section
id="archive-view"
aria-label="Archive"
>Older items.</section>
</ui-tabs><script setup>
import { UiIcon, UiTabs } from "@threadlabs/looma";
</script>
<template>
<UiTabs
label="Workspace views"
>
<template #tab>
<button
value="inbox-view"
> <UiIcon
name="folder"
/> Inbox <span>(3)</span> </button><button
value="archive-view"
>Archive</button>
</template>
<section
id="inbox-view"
aria-label="Inbox"
>Unread and recent items.</section>
<section
id="archive-view"
aria-label="Archive"
>Older items.</section>
</UiTabs>
</template>import { UiIcon, UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="Workspace views"
slots={{
tab: <><button
value="inbox-view"
>{" "}<UiIcon
name="folder"
/> Inbox <span>(3)</span>{" "}</button><button
value="archive-view"
>Archive</button></>,
}}
>
<section
id="inbox-view"
aria-label="Inbox"
>Unread and recent items.</section>
<section
id="archive-view"
aria-label="Archive"
>Older items.</section>
</UiTabs>;
}/** @jsxImportSource preact */
import { UiIcon, UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiTabs
label="Workspace views"
slots={{
tab: <><button
value="inbox-view"
>{" "}<UiIcon
name="folder"
/> Inbox <span>(3)</span>{" "}</button><button
value="archive-view"
>Archive</button></>,
}}
>
<section
id="inbox-view"
aria-label="Inbox"
>Unread and recent items.</section>
<section
id="archive-view"
aria-label="Archive"
>Older items.</section>
</UiTabs>;
}<script>
import { UiIcon, UiTabs } from "@threadlabs/looma";
</script>
{#snippet tab()}
<button
value="inbox-view"
> <UiIcon
name="folder"
/> Inbox <span>(3)</span> </button><button
value="archive-view"
>Archive</button>
{/snippet}
<UiTabs
label="Workspace views"
slots={{ tab }}
>
<section
id="inbox-view"
aria-label="Inbox"
>Unread and recent items.</section>
<section
id="archive-view"
aria-label="Archive"
>Older items.</section>
</UiTabs>import { Component } from "@angular/core";
import { UiIcon, UiTabs } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiTabs, UiIcon],
template: `
<div
ui-tabs
[label]="'Workspace views'"
>
<ng-container ngProjectAs="[slot='tab']">
<button
value="inbox-view"
> <span
ui-icon
name="folder"
></span> Inbox <span>(3)</span> </button><button
value="archive-view"
>Archive</button>
</ng-container>
<section
id="inbox-view"
aria-label="Inbox"
>Unread and recent items.</section>
<section
id="archive-view"
aria-label="Archive"
>Older items.</section>
</div>
`,
})
export default class Example {}Synced group
Tabs in the same group select the same label together, and remember the choice for the next visit.
pnpm add @threadlabs/loomanpm install @threadlabs/loomapnpm devnpm run devCode
<link
rel="component"
href="@threadlabs/looma/components/ui-stack/ui-stack.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-tabs/ui-tabs.html"
>
<ui-stack
gap="s"
>
<ui-tabs
label="Install"
group="package-manager"
>
<section
id="install-pnpm"
aria-label="pnpm"
><code>pnpm add @threadlabs/looma</code></section>
<section
id="install-npm"
aria-label="npm"
><code>npm install @threadlabs/looma</code></section>
</ui-tabs>
<ui-tabs
label="Run"
group="package-manager"
>
<section
id="run-pnpm"
aria-label="pnpm"
><code>pnpm dev</code></section>
<section
id="run-npm"
aria-label="npm"
><code>npm run dev</code></section>
</ui-tabs>
</ui-stack><script setup>
import { UiStack, UiTabs } from "@threadlabs/looma";
</script>
<template>
<UiStack
gap="s"
>
<UiTabs
label="Install"
group="package-manager"
>
<section
id="install-pnpm"
aria-label="pnpm"
><code>pnpm add @threadlabs/looma</code></section>
<section
id="install-npm"
aria-label="npm"
><code>npm install @threadlabs/looma</code></section>
</UiTabs>
<UiTabs
label="Run"
group="package-manager"
>
<section
id="run-pnpm"
aria-label="pnpm"
><code>pnpm dev</code></section>
<section
id="run-npm"
aria-label="npm"
><code>npm run dev</code></section>
</UiTabs>
</UiStack>
</template>import { UiStack, UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="s"
>
<UiTabs
label="Install"
group="package-manager"
>
<section
id="install-pnpm"
aria-label="pnpm"
><code>pnpm add @threadlabs/looma</code></section>
<section
id="install-npm"
aria-label="npm"
><code>npm install @threadlabs/looma</code></section>
</UiTabs>
<UiTabs
label="Run"
group="package-manager"
>
<section
id="run-pnpm"
aria-label="pnpm"
><code>pnpm dev</code></section>
<section
id="run-npm"
aria-label="npm"
><code>npm run dev</code></section>
</UiTabs>
</UiStack>;
}/** @jsxImportSource preact */
import { UiStack, UiTabs } from "@threadlabs/looma";
export default function Example() {
return <UiStack
gap="s"
>
<UiTabs
label="Install"
group="package-manager"
>
<section
id="install-pnpm"
aria-label="pnpm"
><code>pnpm add @threadlabs/looma</code></section>
<section
id="install-npm"
aria-label="npm"
><code>npm install @threadlabs/looma</code></section>
</UiTabs>
<UiTabs
label="Run"
group="package-manager"
>
<section
id="run-pnpm"
aria-label="pnpm"
><code>pnpm dev</code></section>
<section
id="run-npm"
aria-label="npm"
><code>npm run dev</code></section>
</UiTabs>
</UiStack>;
}<script>
import { UiStack, UiTabs } from "@threadlabs/looma";
</script>
<UiStack
gap="s"
>
<UiTabs
label="Install"
group="package-manager"
>
<section
id="install-pnpm"
aria-label="pnpm"
><code>pnpm add @threadlabs/looma</code></section>
<section
id="install-npm"
aria-label="npm"
><code>npm install @threadlabs/looma</code></section>
</UiTabs>
<UiTabs
label="Run"
group="package-manager"
>
<section
id="run-pnpm"
aria-label="pnpm"
><code>pnpm dev</code></section>
<section
id="run-npm"
aria-label="npm"
><code>npm run dev</code></section>
</UiTabs>
</UiStack>import { Component } from "@angular/core";
import { UiStack, UiTabs } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiStack, UiTabs],
template: `
<div
ui-stack
[gap]="'s'"
>
<div
ui-tabs
[label]="'Install'"
[group]="'package-manager'"
>
<section
id="install-pnpm"
aria-label="pnpm"
><code>pnpm add @threadlabs/looma</code></section>
<section
id="install-npm"
aria-label="npm"
><code>npm install @threadlabs/looma</code></section>
</div>
<div
ui-tabs
[label]="'Run'"
[group]="'package-manager'"
>
<section
id="run-pnpm"
aria-label="pnpm"
><code>pnpm dev</code></section>
<section
id="run-npm"
aria-label="npm"
><code>npm run dev</code></section>
</div>
</div>
`,
})
export default class Example {}API
Package: @threadlabs/looma · Native root: div
Attributes
| Name | Description | Property | Type | Default | Allowed values |
|---|---|---|---|---|---|
activation | In auto mode, arrow keys move focus and select the next panel. In manual mode, arrows move focus while Enter or Space selects a panel. Use manual when displaying a panel takes time. | activation | keyword | auto | auto, manual |
density | Compact trades padding and type size for rows that fit more on screen. | density | keyword | comfortable | comfortable, compact |
group | Tabs with the same nonempty group share one selection: choosing a tab selects the tab with the same data-value (or label when no value is authored) in every other Tabs of that group in the document, and the choice is remembered for the next visit. Use it for alternatives a reader picks once, such as a package manager or language. | group | string | | — |
label | The tab list's accessible name, announced when focus enters the list. Name what the tabs switch between. | label | string | Tabs | — |
orientation | horizontal puts the tabs in a row above the panels, moved between with the Left and Right arrow keys; vertical puts them in a column beside the panels, moved between with Up and Down. | orientation | keyword | horizontal | horizontal, vertical |
stretch | Share the full width of the tab list equally between tabs instead of sizing each to its label. | stretch | boolean | false | — |
value | The id or authored data-value of the selected panel, initially or when set externally; empty selects the first panel. Choosing a tab updates it from there and reports a select event. A panel without an id is given a generated one. | value | string | | — |
Framework props
| Name | Type | Default | Allowed values |
|---|---|---|---|
activation | "auto" | "manual" | auto | auto, manual |
density | "comfortable" | "compact" | comfortable | comfortable, compact |
group | string | | — |
label | string | Tabs | — |
orientation | "horizontal" | "vertical" | horizontal | horizontal, vertical |
stretch | boolean | false | — |
value | string | | — |
Events
| Name | Detail | Description |
|---|---|---|
select | object
| Emitted when a selectable option becomes active. |
Slots
| Name | Purpose |
|---|---|
tab | Named content for the tab region. |
default | Default child content. |
Design tokens
Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.
Component tokens
| Token | Declared values | Fallback |
|---|---|---|
--ui-tabs-panel-padding | — | var(--ui-space-4) |
--ui-tabs-tab-min-block-size | var(--ui-control-size) | var(--ui-control-size-sm) | — |
--ui-tabs-tab-padding-block | var(--ui-space-2) | var(--ui-space-1) | — |
--ui-tabs-tab-padding-inline | var(--ui-space-3) | var(--ui-space-2) | — |
Shared tokens
| Token | Fallback |
|---|---|
--ui-accent-line-width | — |
--ui-accent-subtle | — |
--ui-accent-subtle-text | — |
--ui-border-divider | — |
--ui-border-width | — |
--ui-control-size | — |
--ui-control-size-sm | — |
--ui-focus-ring | — |
--ui-focus-width | — |
--ui-font-medium | — |
--ui-font-size-sm | — |
--ui-motion-ease | — |
--ui-motion-fast | — |
--ui-radius-sm | — |
--ui-space-1 | — |
--ui-space-2 | — |
--ui-space-3 | — |
--ui-space-4 | — |
--ui-surface-muted | — |
--ui-text-primary | — |
--ui-text-secondary | — |