Skip to content
Ndop UI

Tabs

Layered sections of content, shown one at a time.

  • v1.0.0
  • WAI-ARIA: Tabs
  • SSR
  • Zoneless
  • 3.1 kB gzip
  • +1.7 kB Angular CDK, shared

Account

Make changes to your account here. Click save when you're done.

import { Component } from '@angular/core';
import { Button } from '@/ui/button/button';
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/ui/card/card';
import { Input } from '@/ui/input/input';
import { Label } from '@/ui/label/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/tabs/tabs';

/**
 * Account settings in two tabs, each a card with a form. The arrow keys
 * move between the tabs; Tab moves on into the shown panel.
 */
@Component({
  selector: 'example-tabs-demo',
  imports: [
    Button,
    Card,
    CardContent,
    CardDescription,
    CardFooter,
    CardHeader,
    CardTitle,
    Input,
    Label,
    Tabs,
    TabsContent,
    TabsList,
    TabsTrigger,
  ],
  template: `
    <div ndTabs defaultValue="account" class="w-sm max-w-full">
      <div ndTabsList aria-label="Settings">
        <button ndTabsTrigger value="account">Account</button>
        <button ndTabsTrigger value="password">Password</button>
      </div>
      <div ndTabsContent value="account">
        <div ndCard>
          <div ndCardHeader>
            <h2 ndCardTitle>Account</h2>
            <p ndCardDescription>
              Make changes to your account here. Click save when you're done.
            </p>
          </div>
          <div ndCardContent class="grid gap-6">
            <div class="grid gap-3">
              <label ndLabel for="tabs-demo-name">Name</label>
              <input ndInput id="tabs-demo-name" value="Pedro Duarte" />
            </div>
            <div class="grid gap-3">
              <label ndLabel for="tabs-demo-username">Username</label>
              <input ndInput id="tabs-demo-username" value="@peduarte" />
            </div>
          </div>
          <div ndCardFooter>
            <button ndButton>Save changes</button>
          </div>
        </div>
      </div>
      <div ndTabsContent value="password">
        <div ndCard>
          <div ndCardHeader>
            <h2 ndCardTitle>Password</h2>
            <p ndCardDescription>
              Change your password here. After saving, you'll be logged out.
            </p>
          </div>
          <div ndCardContent class="grid gap-6">
            <div class="grid gap-3">
              <label ndLabel for="tabs-demo-current">Current password</label>
              <input ndInput id="tabs-demo-current" type="password" />
            </div>
            <div class="grid gap-3">
              <label ndLabel for="tabs-demo-new">New password</label>
              <input ndInput id="tabs-demo-new" type="password" />
            </div>
          </div>
          <div ndCardFooter>
            <button ndButton>Save password</button>
          </div>
        </div>
      </div>
    </div>
  `,
})
export class TabsDemoExample {}

Tabs split related content into sections shown one at a time, such as the panes of a settings page. Only the selected tab is in the tab order; the arrow keys move between tabs and show each one's panel, and Tab moves on into the panel. line underlines the selected tab instead of raising it, and orientation="vertical" stacks the tabs beside their panels.

Installation

npx @ndop/cli add tabs

Usage

import { Button } from '@/ui/button/button';
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/ui/card/card';
import { Input } from '@/ui/input/input';
import { Label } from '@/ui/label/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/tabs/tabs';
<div ndTabs defaultValue="account" class="w-sm max-w-full">
  <div ndTabsList aria-label="Settings">
    <button ndTabsTrigger value="account">Account</button>
    <button ndTabsTrigger value="password">Password</button>
  </div>
  <div ndTabsContent value="account">
    <div ndCard>
      <div ndCardHeader>
        <h2 ndCardTitle>Account</h2>
        <p ndCardDescription>
          Make changes to your account here. Click save when you're done.
        </p>
      </div>
      <div ndCardContent class="grid gap-6">
        <div class="grid gap-3">
          <label ndLabel for="tabs-demo-name">Name</label>
          <input ndInput id="tabs-demo-name" value="Pedro Duarte" />
        </div>
        <div class="grid gap-3">
          <label ndLabel for="tabs-demo-username">Username</label>
          <input ndInput id="tabs-demo-username" value="@peduarte" />
        </div>
      </div>
      <div ndCardFooter>
        <button ndButton>Save changes</button>
      </div>
    </div>
  </div>
  <div ndTabsContent value="password">
    <div ndCard>
      <div ndCardHeader>
        <h2 ndCardTitle>Password</h2>
        <p ndCardDescription>
          Change your password here. After saving, you'll be logged out.
        </p>
      </div>
      <div ndCardContent class="grid gap-6">
        <div class="grid gap-3">
          <label ndLabel for="tabs-demo-current">Current password</label>
          <input ndInput id="tabs-demo-current" type="password" />
        </div>
        <div class="grid gap-3">
          <label ndLabel for="tabs-demo-new">New password</label>
          <input ndInput id="tabs-demo-new" type="password" />
        </div>
      </div>
      <div ndCardFooter>
        <button ndButton>Save password</button>
      </div>
    </div>
  </div>
</div>

Examples

Tabs line

Tabs underlined instead of raised, with the line variant.

Your project at a glance.
import { Component } from '@angular/core';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/tabs/tabs';

/** Tabs underlined instead of raised, with the line variant. */
@Component({
  selector: 'example-tabs-line',
  imports: [Tabs, TabsContent, TabsList, TabsTrigger],
  template: `
    <div ndTabs defaultValue="overview" class="w-sm max-w-full">
      <div ndTabsList variant="line" aria-label="Project">
        <button ndTabsTrigger value="overview">Overview</button>
        <button ndTabsTrigger value="analytics">Analytics</button>
        <button ndTabsTrigger value="reports" disabled>Reports</button>
      </div>
      <div ndTabsContent value="overview" class="text-sm text-muted-foreground">
        Your project at a glance.
      </div>
      <div
        ndTabsContent
        value="analytics"
        class="text-sm text-muted-foreground"
      >
        Visitors and conversions over time.
      </div>
      <div ndTabsContent value="reports" class="text-sm text-muted-foreground">
        Reports are on the paid plan.
      </div>
    </div>
  `,
})
export class TabsLineExample {}

Tabs vertical

Tabs stacked beside their panels, selected with Enter or Space.

Language, time zone and start page.
import { Component } from '@angular/core';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/ui/tabs/tabs';

/**
 * Tabs stacked beside their panels. With manual activation, the arrow
 * keys only move focus; Enter or Space shows the focused tab's panel.
 */
@Component({
  selector: 'example-tabs-vertical',
  imports: [Tabs, TabsContent, TabsList, TabsTrigger],
  template: `
    <div
      ndTabs
      defaultValue="general"
      orientation="vertical"
      activationMode="manual"
      class="w-md max-w-full"
    >
      <div ndTabsList aria-label="Preferences">
        <button ndTabsTrigger value="general">General</button>
        <button ndTabsTrigger value="notifications">Notifications</button>
        <button ndTabsTrigger value="privacy">Privacy</button>
      </div>
      <div ndTabsContent value="general" class="text-sm">
        Language, time zone and start page.
      </div>
      <div ndTabsContent value="notifications" class="text-sm">
        Email and push notifications.
      </div>
      <div ndTabsContent value="privacy" class="text-sm">
        Who can see your profile and activity.
      </div>
    </div>
  `,
})
export class TabsVerticalExample {}

Accessibility

Implements the Tabs pattern of the WAI-ARIA Authoring Practices.

Keyboard

Keys the component handles.

Key Action
TabMoves focus into the tab list, to the selected tab; then to the active panel.
ArrowRightorArrowLeftIn a horizontal list, moves focus to the next or previous enabled tab, wrapping; selects it if automatic.
ArrowDownorArrowUpIn a vertical list, moves focus to the next or previous enabled tab, wrapping; selects it if automatic.
HomeMoves focus to the first enabled tab; selects it if automatic.
EndMoves focus to the last enabled tab; selects it if automatic.
EnterorSpaceSelects the focused tab.

Focus

  • The selected tab is the list's only tab stop; arrow keys move focus without leaving the list, and disabled tabs are skipped.
  • Clicking a tab focuses and selects it.

Right-to-left

In a horizontal list in a right-to-left layout, ArrowLeft moves to the next tab and ArrowRight to the previous one. Direction comes from the nearest dir attribute.

API reference

Tabs

[ndTabs]

Tabs (@ndop/primitives/tabs): layered sections of content, shown one at a time. Its list of tabs and its panels go inside it.

Tabs binding Type Default
[class]
Extra classes, merged last so they win.
string''
[defaultValue]
Initial value when it isn't bound.
string | nullnull
[orientation]
The tab list's layout: which arrow keys move between tabs.
'horizontal' | 'vertical''horizontal'
[activationMode]
Whether moving focus to a tab selects it.
'automatic' | 'manual''automatic'
[(value)]
The selected tab's value, or null before one is selected. Two-way bindable with [(value)].
string | nullnull
Data attribute Values
data-slottabs
data-orientation —

TabsList

[ndTabsList]role tablist

The row (or column) of tabs. line underlines the selected tab instead of raising it. Name it with aria-label.

Manages aria-orientation.

TabsList binding Type Default
[variant]
Visual style.
'default' | 'line''default'
[class]
Extra classes, merged last so they win.
string''
Data attribute Values
data-slottabs-list
data-variant —
data-orientation —

TabsTrigger

button[ndTabsTrigger]role tab

One tab, on a button: it shows the panel with the same value.

Manages aria-selected, aria-controls, id, tabindex.

TabsTrigger binding Type Default
[class]
Extra classes, merged last so they win.
string''
[value]required
The value of the panel it shows.
string—
[disabled]
Disables it; arrow keys skip it.
booleanfalse
Data attribute Values
data-slottabs-trigger
data-state —
data-disabled —
data-orientation —

TabsContent

[ndTabsContent]role tabpanel

The panel of the tab with the same value, hidden while that tab isn't selected. It can take focus, so it shows a focus ring.

Manages aria-labelledby, id, tabindex, hidden.

TabsContent binding Type Default
[class]
Extra classes, merged last so they win.
string''
[value]required
The value of the tab that shows it.
string—
Data attribute Values
data-slottabs-content
data-state —
data-orientation —

Agent notes

Do

  • Keep tab labels short, and the content of each panel independent.
  • Name the list with aria-label when nothing on the page names it.
  • Use activationMode="manual" when showing a panel is slow, so moving through the tabs doesn't load each one.

Don't

  • Don't use tabs to step through a process in order; use a form with steps.
  • Don't use them for site navigation between pages; use links, or Navigation Menu.