Tabs

Switch between panels of related content, one at a time.

import { โ€ฆ } from '@inlaylab/ivorie/tabs'
AccountPasswordMake changes to your account here.
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

CLInpm
npx ivorie add 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

ABPanel A

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

OverviewAnalyticsReportsOverview panel
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

ProfileBillingNotificationsProfile panel
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

HomeSettingsHome panel
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

StableBetaStable panel
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" and aria-orientation. Triggers have role="tab", aria-selected and aria-controls. Panels have role="tabpanel", aria-labelledby and tabindex="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-list an aria-label when 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 value does not reset the tab. Accept the change, or set the value on the next tick.
  • IvTabs.select(value) selects a tab from code. IvTabsTrigger.isActive is a signal.