# Tabs

Use `ui-tabs` when users need to move between a small number of sibling sections inside the same page, panel, or drawer. The component focuses on the tablist itself, so the surrounding view is responsible for swapping the content that each tab represents.

## Import
```ts
import { TabsComponent, type Tab } from 'ui';
```

## Basic local navigation
```ts
import { Component } from '@angular/core';
import { TabsComponent, type Tab } from 'ui';

type BasicTab = Tab & {
  description: string;
};

@Component({
  selector: 'app-tabs-basic-demo',
  standalone: true,
  imports: [TabsComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:40rem">
      <ui-tabs
        [tabs]="tabs"
        [(selectedTabId)]="selectedTabId"
        appearance="subtle"
        variant="primary"
      />

      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.9375rem;font-weight:600">{{ selectedTab.label }}</div>
        <div
          style="margin-top:0.375rem;font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground2-rest)"
        >
          {{ selectedTab.description }}
        </div>
      </div>
    </div>
  `,
})
export class TabsBasicDemoComponent {
  protected readonly tabs: BasicTab[] = [
    {
      id: 'overview',
      label: 'Overview',
      icon: 'book',
      description:
        'Use tabs to switch between closely related views without leaving the current surface.',
    },
    {
      id: 'activity',
      label: 'Activity',
      icon: 'history',
      description:
        'The active tab can drive the content below while staying a lightweight local navigation pattern.',
    },
    {
      id: 'settings',
      label: 'Settings',
      icon: 'settings',
      description:
        'This is a good fit for panels, drawers, dashboards, and detail views with a few sibling sections.',
    },
  ];

  protected selectedTabId: string | number = this.tabs[0].id;

  protected get selectedTab(): BasicTab {
    return this.tabs.find(tab => tab.id === this.selectedTabId) ?? this.tabs[0];
  }
}
```

## Appearance, size, and full-width layout
```ts
import { Component } from '@angular/core';
import { TabsComponent, type Tab } from 'ui';

const tabs: Tab[] = [
  { id: 'files', label: 'Files' },
  { id: 'people', label: 'People' },
  { id: 'notes', label: 'Notes' },
];

@Component({
  selector: 'app-tabs-appearance-layout-demo',
  standalone: true,
  imports: [TabsComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:44rem">
      <div
        style="display:flex;flex-direction:column;gap:0.875rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.9375rem;font-weight:600">Subtle surface tabs</div>
        <ui-tabs [tabs]="tabs" appearance="subtle" variant="primary" size="medium" />
      </div>

      <div
        style="display:flex;flex-direction:column;gap:0.875rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <div style="font-size:0.9375rem;font-weight:600">Full-width section tabs</div>
        <ui-tabs
          [tabs]="tabs"
          appearance="filled"
          variant="secondary"
          size="large"
          [fullWidth]="true"
          shape="circular"
        />
      </div>
    </div>
  `,
})
export class TabsAppearanceLayoutDemoComponent {
  protected readonly tabs = tabs;
}
```

## Vertical orientation
```ts
import { Component } from '@angular/core';
import { TabsComponent, type Tab } from 'ui';

type VerticalTab = Tab & {
  title: string;
  body: string;
};

@Component({
  selector: 'app-tabs-orientation-demo',
  standalone: true,
  imports: [TabsComponent],
  template: `
    <div
      style="display:grid;grid-template-columns:minmax(12rem,14rem) minmax(0,1fr);gap:1rem;align-items:start;width:100%;max-width:48rem"
    >
      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-tabs
          [tabs]="tabs"
          [(selectedTabId)]="selectedTabId"
          orientation="vertical"
          appearance="transparent"
          variant="primary"
        />
      </div>

      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest);min-width:0"
      >
        <div style="font-size:0.9375rem;font-weight:600">{{ selectedTab.title }}</div>
        <div
          style="margin-top:0.375rem;font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground2-rest)"
        >
          {{ selectedTab.body }}
        </div>
      </div>
    </div>
  `,
})
export class TabsOrientationDemoComponent {
  protected readonly tabs: VerticalTab[] = [
    {
      id: 'general',
      label: 'General',
      title: 'General workspace settings',
      body: 'Vertical tabs work well when labels are longer or when the content area beside them is the main focus.',
    },
    {
      id: 'access',
      label: 'Access and roles',
      title: 'Access and roles',
      body: 'This pattern fits settings pages, onboarding surfaces, and detail panels with a few sibling groups.',
    },
    {
      id: 'retention',
      label: 'Retention policy',
      title: 'Retention policy',
      body: 'The tablist stays local to the panel, so users can move between related sections without a route change.',
    },
  ];

  protected selectedTabId: string | number = this.tabs[0].id;

  protected get selectedTab(): VerticalTab {
    return this.tabs.find(tab => tab.id === this.selectedTabId) ?? this.tabs[0];
  }
}
```

## Disabled and closable tabs
```ts
import { Component } from '@angular/core';
import { TabsComponent, type Tab } from 'ui';

@Component({
  selector: 'app-tabs-options-demo',
  standalone: true,
  imports: [TabsComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:44rem">
      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-tabs
          [tabs]="editableTabs"
          [(selectedTabId)]="selectedTabId"
          appearance="subtle"
          variant="primary"
          (tabClose)="closeTab($event)"
        />
      </div>

      <div
        style="padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <p
          style="margin:0 0 0.5rem;font-size:0.75rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-neutral-foreground2-rest)"
        >
          Current tabs
        </p>
        <div
          style="display:flex;flex-direction:column;gap:0.45rem;font-size:0.875rem;line-height:1.4"
        >
          @for (tab of editableTabs; track tab.id) {
            <div style="display:flex;justify-content:space-between;gap:1rem">
              <span style="color:var(--color-neutral-foreground2-rest)">{{ tab.label }}</span>
              <strong style="font-weight:600;color:var(--color-neutral-foreground-rest)">{{
                tab.disabled ? 'Disabled' : tab.closable ? 'Closable' : 'Pinned'
              }}</strong>
            </div>
          }
        </div>
      </div>
    </div>
  `,
})
export class TabsOptionsDemoComponent {
  protected editableTabs: Tab[] = [
    { id: 'summary', label: 'Summary', icon: 'book' },
    { id: 'draft', label: 'Draft', icon: 'document', closable: true },
    { id: 'review', label: 'Needs review', icon: 'edit', closable: true },
    { id: 'archive', label: 'Archive', icon: 'archive', disabled: true },
  ];

  protected selectedTabId: string | number = this.editableTabs[0].id;

  protected closeTab(tab: Tab): void {
    this.editableTabs = this.editableTabs.filter(current => current.id !== tab.id);

    if (this.selectedTabId === tab.id && this.editableTabs.length > 0) {
      this.selectedTabId = this.editableTabs[0].id;
    }
  }
}
```

## Overflow entry point
```ts
import { Component } from '@angular/core';
import { ButtonComponent, TabsComponent, type Tab } from 'ui';

@Component({
  selector: 'app-tabs-overflow-demo',
  standalone: true,
  imports: [ButtonComponent, TabsComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:42rem">
      <div
        style="padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <ui-tabs
          [tabs]="visibleTabs"
          [showMoreButton]="true"
          [(selectedTabId)]="selectedTabId"
          appearance="subtle"
          variant="primary"
        >
          <ui-button moreButton type="button" variant="secondary" appearance="subtle" size="small">
            More
          </ui-button>
        </ui-tabs>
      </div>

      <div
        style="padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <p
          style="margin:0 0 0.5rem;font-size:0.75rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-neutral-foreground2-rest)"
        >
          Hidden destinations
        </p>
        <div style="font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground-rest)">
          {{ hiddenLabels }}
        </div>
      </div>
    </div>
  `,
})
export class TabsOverflowDemoComponent {
  protected readonly visibleTabs: Tab[] = [
    { id: 'overview', label: 'Overview' },
    { id: 'timeline', label: 'Timeline' },
    { id: 'files', label: 'Files' },
  ];

  private readonly hiddenTabs: Tab[] = [
    { id: 'approvals', label: 'Approvals' },
    { id: 'audit', label: 'Audit log' },
    { id: 'history', label: 'History' },
  ];

  protected selectedTabId: string | number = this.visibleTabs[0].id;

  protected get hiddenLabels(): string {
    return this.hiddenTabs.map(tab => tab.label).join(', ');
  }
}
```

## Workspace panel composition
```ts
import { Component } from '@angular/core';
import { TabsComponent, type Tab } from 'ui';

type WorkspaceTab = Tab & {
  headline: string;
  summary: string;
  statLabel: string;
  statValue: string;
};

@Component({
  selector: 'app-tabs-workspace-panel-demo',
  standalone: true,
  imports: [TabsComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:44rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <div style="display:flex;flex-direction:column;gap:0.375rem">
        <div style="font-size:1rem;font-weight:600">Workspace detail panel</div>
        <div
          style="font-size:0.875rem;color:var(--color-neutral-foreground2-rest);line-height:1.45"
        >
          A realistic tabs surface uses the tablist as local navigation and swaps the panel content
          underneath it.
        </div>
      </div>

      <ui-tabs
        [tabs]="tabs"
        [(selectedTabId)]="selectedTabId"
        appearance="subtle"
        variant="primary"
      />

      <div
        style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;padding:0.875rem 1rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:0.875rem;background:var(--color-neutral-background-rest)"
      >
        <div style="flex:1 1 16rem;display:flex;flex-direction:column;gap:0.25rem;min-width:14rem">
          <span
            style="font-size:0.75rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--color-neutral-foreground2-rest)"
          >
            {{ activeTab.statLabel }}
          </span>
          <strong
            style="font-size:1.125rem;font-weight:600;color:var(--color-neutral-foreground-rest)"
          >
            {{ activeTab.statValue }}
          </strong>
        </div>
        <div style="flex:2 1 20rem;min-width:16rem">
          <div style="font-size:0.9375rem;font-weight:600">{{ activeTab.headline }}</div>
          <div
            style="margin-top:0.375rem;font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground2-rest)"
          >
            {{ activeTab.summary }}
          </div>
        </div>
      </div>
    </div>
  `,
})
export class TabsWorkspacePanelDemoComponent {
  protected readonly tabs: WorkspaceTab[] = [
    {
      id: 'overview',
      label: 'Overview',
      icon: 'book',
      headline: 'Workspace health snapshot',
      summary:
        'Use a short overview tab for current status, owners, and top-level signals before the user dives into lower-level sections.',
      statLabel: 'Active projects',
      statValue: '12',
    },
    {
      id: 'members',
      label: 'Members',
      icon: 'people',
      headline: 'Members and roles',
      summary:
        'Tabs help separate role management and access details from the main summary without moving the user to a different route.',
      statLabel: 'Contributors',
      statValue: '38',
    },
    {
      id: 'automation',
      label: 'Automation',
      icon: 'design_ideas',
      headline: 'Workflow automation',
      summary:
        'Automation settings, rules, and notifications often belong in neighboring tabs because they are related but not needed all at once.',
      statLabel: 'Active rules',
      statValue: '7',
    },
  ];

  protected selectedTabId: string | number = this.tabs[0].id;

  protected get activeTab(): WorkspaceTab {
    return this.tabs.find(tab => tab.id === this.selectedTabId) ?? this.tabs[0];
  }
}
```

## Accessibility

### Tablist semantics
`ui-tabs` exposes a container with `role="tablist"` and sets `aria-orientation` from the `orientation` input. Each tab item is rendered through the underlying node button model and reflects the selected state through the node selection semantics.

### Keyboard navigation
| Key | Action |
| --- | --- |
| Arrow Left / Right | Moves between enabled tabs in horizontal mode. |
| Arrow Up / Down | Moves between enabled tabs in vertical mode. |
| Home | Moves to the first enabled tab. |
| End | Moves to the last enabled tab. |
| Tab | Moves focus into or out of the tablist according to normal document order. |

### Selection model
If `selectedTabId` is not provided, the component selects the first tab automatically. Disabled tabs are skipped during arrow-key navigation, and clicking the currently selected tab does not emit another change event.
