Tabs
Switch between panels of related content, one at a time.
import { โฆ } from '@inlaylab/ivorie/tabs'import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_TABS_IMPORTS } from '@inlaylab/ivorie/tabs';
@Component({
selector: 'demo-tabs',
imports: [IV_TABS_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-tabs defaultValue="account" class="w-80">
<iv-tabs-list>
<iv-tabs-trigger value="account">Account</iv-tabs-trigger>
<iv-tabs-trigger value="password">Password</iv-tabs-trigger>
</iv-tabs-list>
<iv-tabs-content value="account">Make changes to your account here.</iv-tabs-content>
<iv-tabs-content value="password">Change your password here.</iv-tabs-content>
</iv-tabs>
`,
})
export default class TabsDemo {}
Installation
npx ivorie add tabsimport { โฆ } from '@inlaylab/ivorie/tabs'Tabs with horizontal and vertical layout and two list styles (default pill, line underline). Ports the shadcn/ui Tabs.
Usage
import { IV_TABS_IMPORTS } from '@inlaylab/ivorie/tabs';<iv-tabs defaultValue="account">
<iv-tabs-list>
<iv-tabs-trigger value="account">Account</iv-tabs-trigger>
<iv-tabs-trigger value="password">Password</iv-tabs-trigger>
</iv-tabs-list>
<iv-tabs-content value="account">Account settings.</iv-tabs-content>
<iv-tabs-content value="password">Change your password.</iv-tabs-content>
</iv-tabs>Examples
Controlled
Active tab: a
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_TABS_IMPORTS } from '@inlaylab/ivorie/tabs';
@Component({
selector: 'demo-tabs-controlled',
imports: [IV_TABS_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-tabs [(value)]="active" class="w-80">
<iv-tabs-list>
<iv-tabs-trigger value="a">A</iv-tabs-trigger>
<iv-tabs-trigger value="b">B</iv-tabs-trigger>
</iv-tabs-list>
<iv-tabs-content value="a">Panel A</iv-tabs-content>
<iv-tabs-content value="b">Panel B</iv-tabs-content>
</iv-tabs>
<p class="mt-3 text-sm text-muted-foreground">Active tab: {{ active() }}</p>
`,
})
export default class TabsControlledDemo {
protected readonly active = signal('a');
}
Line
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_TABS_IMPORTS } from '@inlaylab/ivorie/tabs';
@Component({
selector: 'demo-tabs-line',
imports: [IV_TABS_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-tabs defaultValue="overview" class="w-96">
<iv-tabs-list variant="line">
<iv-tabs-trigger value="overview">Overview</iv-tabs-trigger>
<iv-tabs-trigger value="analytics">Analytics</iv-tabs-trigger>
<iv-tabs-trigger value="reports">Reports</iv-tabs-trigger>
</iv-tabs-list>
<iv-tabs-content value="overview">Overview panel</iv-tabs-content>
<iv-tabs-content value="analytics">Analytics panel</iv-tabs-content>
<iv-tabs-content value="reports">Reports panel</iv-tabs-content>
</iv-tabs>
`,
})
export default class TabsLineDemo {}
Vertical
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_TABS_IMPORTS } from '@inlaylab/ivorie/tabs';
@Component({
selector: 'demo-tabs-vertical',
imports: [IV_TABS_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-tabs defaultValue="profile" orientation="vertical" class="w-96 gap-4">
<iv-tabs-list>
<iv-tabs-trigger value="profile">Profile</iv-tabs-trigger>
<iv-tabs-trigger value="billing">Billing</iv-tabs-trigger>
<iv-tabs-trigger value="notifications">Notifications</iv-tabs-trigger>
</iv-tabs-list>
<iv-tabs-content value="profile">Profile panel</iv-tabs-content>
<iv-tabs-content value="billing">Billing panel</iv-tabs-content>
<iv-tabs-content value="notifications">Notifications panel</iv-tabs-content>
</iv-tabs>
`,
})
export default class TabsVerticalDemo {}
Icons
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LucideHouse, LucideSettings } from '@lucide/angular';
import { IV_TABS_IMPORTS } from '@inlaylab/ivorie/tabs';
@Component({
selector: 'demo-tabs-icons',
imports: [IV_TABS_IMPORTS, LucideHouse, LucideSettings],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-tabs defaultValue="home" class="w-80">
<iv-tabs-list>
<iv-tabs-trigger value="home"><svg lucideHouse></svg>Home</iv-tabs-trigger>
<iv-tabs-trigger value="settings"><svg lucideSettings></svg>Settings</iv-tabs-trigger>
</iv-tabs-list>
<iv-tabs-content value="home">Home panel</iv-tabs-content>
<iv-tabs-content value="settings">Settings panel</iv-tabs-content>
</iv-tabs>
`,
})
export default class TabsIconsDemo {}
Disabled
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_TABS_IMPORTS } from '@inlaylab/ivorie/tabs';
@Component({
selector: 'demo-tabs-disabled',
imports: [IV_TABS_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-tabs defaultValue="stable" class="w-80">
<iv-tabs-list>
<iv-tabs-trigger value="stable">Stable</iv-tabs-trigger>
<iv-tabs-trigger value="beta" [disabled]="true">Beta</iv-tabs-trigger>
</iv-tabs-list>
<iv-tabs-content value="stable">Stable panel</iv-tabs-content>
<iv-tabs-content value="beta">Beta panel</iv-tabs-content>
</iv-tabs>
`,
})
export default class TabsDisabledDemo {}
API
Every part also accepts class.
iv-tabs
| Name | Type | Default | Description |
|---|---|---|---|
value |
string |
- | Active tab. Bind with [(value)] for controlled use. |
defaultValue |
string |
'' |
Initial active tab. Used only while value is not set. |
orientation |
'horizontal' | 'vertical' |
'horizontal' |
Layout and arrow-key axis. |
Outputs:
| Name | Payload | Description |
|---|---|---|
valueChange |
string |
The user selected a different tab. |
iv-tabs-list
| Name | Type | Default | Description |
|---|---|---|---|
variant |
'default' | 'line' |
'default' |
default is a muted pill. line is an underline. |
iv-tabs-trigger
| Name | Type | Default | Description |
|---|---|---|---|
value |
string |
required | Identifies the tab. Must match an iv-tabs-content value. |
disabled |
boolean |
false |
Blocks activation and is skipped by arrow keys. |
iv-tabs-content
| Name | Type | Default | Description |
|---|---|---|---|
value |
string |
required | The trigger value that shows this panel. |
Accessibility
- The list has
role="tablist"andaria-orientation. Triggers haverole="tab",aria-selectedandaria-controls. Panels haverole="tabpanel",aria-labelledbyandtabindex="0". - Activation is automatic: arrow keys move focus and select the tab.
- Left and Right move in horizontal mode (flipped in RTL). Up and Down move in vertical mode. Focus wraps and skips disabled tabs.
- Home and End go to the first and last enabled tab. Enter and Space also select.
- Only the selected tab is in the Tab order. If none is selected, the first enabled tab is.
- Disabled triggers set
aria-disabled. - Give
iv-tabs-listanaria-labelwhen the tabs have no visible heading. - Tabs nested in a panel keep their own keys. Arrows move only within their own list.
Notes
- Panels use
hidden. They stay in the DOM and are not re-created on switch. - A parent that rejects a change and keeps the same
valuedoes not reset the tab. Accept the change, or set the value on the next tick. IvTabs.select(value)selects a tab from code.IvTabsTrigger.isActiveis a signal.