آنلاین

Tabs

Overview

Tabها sectionهای layered content را نمایش می‌دهند که هر بار فقط یک panel visible است. کاربران با کلیک روی tab buttonها یا با استفاده از arrow keyها برای navigate کردن tab list، بین panelها switch می‌کنند.

ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs>
  <div ngTabList selectionMode="follow" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  border-radius: 0.5rem;
  border: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTabList] {
  padding: 0;
  display: flex;
  list-style: none;
  position: relative;
  border-bottom: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 0.75rem 0;
  cursor: pointer;
  text-align: center;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
}

[ngTab][aria-selected='false']:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  border-top-left-radius: 0.5rem;
  border-top-right-radius: 0.5rem;
}

.sliding-window {
  width: 300%;
  display: flex;
  transition: all 0.2s ease-in-out;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  min-height: 100px;
  flex: 1;
  border-bottom-right-radius: 0.5rem;
  border-bottom-left-radius: 0.5rem;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="material-tabs">
  <div ngTabList selectionMode="follow" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
    <div class="bottom-border"></div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  border-radius: 0.5rem;
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTabList] {
  padding: 0;
  display: flex;
  list-style: none;
  position: relative;
  border-bottom: 1px solid color-mix(in srgb, var(--primary-contrast) 20%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem 0;
  cursor: pointer;
  text-align: center;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
}

[ngTab]:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--bright-blue);
}

.bottom-border {
  position: absolute;
  pointer-events: none;
  left: 0;
  bottom: 0;
  height: 3px;
  width: calc(100% / 3);
  background-color: var(--bright-blue);
  transition: all 0.2s ease-in-out;
  transform: translateX(0%);
  border-top-left-radius: 2px;
  border-top-right-radius: 2px;
}

[ngTab]:nth-child(1)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(0%);
}
[ngTab]:nth-child(2)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(100%);
}
[ngTab]:nth-child(3)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(200%);
}

.sliding-window {
  width: 300%;
  display: flex;
  transition: all 0.2s ease-in-out;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  min-height: 100px;
  flex: 1;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="retro-tabs">
  <div ngTabList selectionMode="follow" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
  </div>

  <div ngTabPanel value="movie">
    <ng-template ngTabContent>Line 1</ng-template>
  </div>

  <div ngTabPanel value="theatres">
    <ng-template ngTabContent>Line 2</ng-template>
  </div>

  <div ngTabPanel value="showtimes">
    <ng-template ngTabContent>Line 3</ng-template>
  </div>
</div>
css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;

  font-family: 'Press Start 2P';
  --retro-button-color: var(--bright-blue);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[ngTabs] {
  width: 600px;
}

[ngTabList] {
  gap: 1rem;
  display: flex;
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem;
  cursor: pointer;
  text-align: center;
  color: #000;
  background-color: #fff;
  box-shadow: var(--retro-clickable-shadow);
  transition:
    transform 0.1s,
    box-shadow 0.1s;
}

[ngTab]:focus,
[ngTab]:hover {
  transform: translate(1px, 1px);
}

[ngTab]:focus {
  outline-offset: 2px;
  outline: 4px dashed var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 50%, #fff);
}

[ngTab]:active,
[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  box-shadow: var(--retro-pressed-shadow);
  transform: translate(4px, 4px);
}

[ngTabPanel] {
  flex: 1;
  font-size: 1.5rem;
  color: #000;
  background-color: #fff;
  padding: 1rem;
  margin-top: 1rem;
  min-height: 100px;
  display: grid;
  place-items: center;
  box-shadow: var(--retro-flat-shadow);
  background: linear-gradient(rgba(0, 0, 0, 0.3) 50%, rgba(0, 0, 0, 0.1) 50%);
  background-color: color-mix(in srgb, var(--bright-blue) 10%, #fff);
  background-size: 100% 4px;
}

[ngTabPanel]:focus {
  outline-offset: 4px;
  outline: 4px dashed var(--bright-blue);
}

[ngTabPanel][inert] {
  display: none;
}

Usage

Tabها برای سازماندهی محتوای مرتبط در sectionهای متمایز مناسب‌اند، جایی که کاربران بین viewها یا categoryهای مختلف switch می‌کنند.

از tab استفاده کنید وقتی:

  • محتوای مرتبط را در sectionهای متمایز سازماندهی می‌کنید.
  • settings panelهایی با چند category می‌سازید.
  • documentationای با چند topic می‌سازید.
  • dashboardهایی با viewهای مختلف پیاده‌سازی می‌کنید.
  • contentای نمایش می‌دهید که کاربران باید بین contextها switch کنند.

از tab پرهیز کنید وقتی:

  • formهای sequential یا wizard می‌سازید \(از stepper pattern استفاده کنید\).
  • بین pageها navigate می‌کنید \(از router navigation استفاده کنید\).
  • فقط یک content section نمایش می‌دهید \(نیازی به tab نیست\).
  • بیش از 7-8 tab دارید \(layout متفاوتی را در نظر بگیرید\).

قابلیت‌ها

  • Selection modeها - tabها با focus به صورت خودکار activate شوند یا activation دستی لازم داشته باشند.
  • Keyboard navigation - arrow keyها، Home و End برای navigation کارآمد بین tabها.
  • Orientation - layoutهای tab list افقی یا عمودی.
  • Lazy content - tab panelها فقط وقتی برای اولین بار activate شوند render می‌شوند.
  • Disabled tabs - tabهای جداگانه را با focus management غیرفعال کنید.
  • Focus modeها - strategyهای focus مربوط به roving tabindex یا activedescendant.
  • RTL support - navigation برای زبان‌های راست‌به‌چپ.

مثال‌ها

Selection follows focus

وقتی selection از focus پیروی کند، tabها به محض navigate کردن با arrow keyها activate می‌شوند. این behavior feedback فوری فراهم می‌کند و برای content سبک مناسب است.

ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs>
  <div ngTabList selectionMode="follow" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  border-radius: 0.5rem;
  border: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTabList] {
  padding: 0;
  display: flex;
  list-style: none;
  position: relative;
  border-bottom: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 0.75rem 0;
  cursor: pointer;
  text-align: center;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
}

[ngTab][aria-selected='false']:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  border-top-left-radius: 0.5rem;
  border-top-right-radius: 0.5rem;
}

.sliding-window {
  width: 300%;
  display: flex;
  transition: all 0.2s ease-in-out;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  min-height: 100px;
  flex: 1;
  border-bottom-right-radius: 0.5rem;
  border-bottom-left-radius: 0.5rem;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="material-tabs">
  <div ngTabList selectionMode="follow" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
    <div class="bottom-border"></div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  border-radius: 0.5rem;
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTabList] {
  padding: 0;
  display: flex;
  list-style: none;
  position: relative;
  border-bottom: 1px solid color-mix(in srgb, var(--primary-contrast) 20%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem 0;
  cursor: pointer;
  text-align: center;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
}

[ngTab]:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--bright-blue);
}

.bottom-border {
  position: absolute;
  pointer-events: none;
  left: 0;
  bottom: 0;
  height: 3px;
  width: calc(100% / 3);
  background-color: var(--bright-blue);
  transition: all 0.2s ease-in-out;
  transform: translateX(0%);
  border-top-left-radius: 2px;
  border-top-right-radius: 2px;
}

[ngTab]:nth-child(1)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(0%);
}
[ngTab]:nth-child(2)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(100%);
}
[ngTab]:nth-child(3)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(200%);
}

.sliding-window {
  width: 300%;
  display: flex;
  transition: all 0.2s ease-in-out;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  min-height: 100px;
  flex: 1;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="retro-tabs">
  <div ngTabList selectionMode="follow" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
  </div>

  <div ngTabPanel value="movie">
    <ng-template ngTabContent>Line 1</ng-template>
  </div>

  <div ngTabPanel value="theatres">
    <ng-template ngTabContent>Line 2</ng-template>
  </div>

  <div ngTabPanel value="showtimes">
    <ng-template ngTabContent>Line 3</ng-template>
  </div>
</div>
css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;

  font-family: 'Press Start 2P';
  --retro-button-color: var(--bright-blue);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[ngTabs] {
  width: 600px;
}

[ngTabList] {
  gap: 1rem;
  display: flex;
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem;
  cursor: pointer;
  text-align: center;
  color: #000;
  background-color: #fff;
  box-shadow: var(--retro-clickable-shadow);
  transition:
    transform 0.1s,
    box-shadow 0.1s;
}

[ngTab]:focus,
[ngTab]:hover {
  transform: translate(1px, 1px);
}

[ngTab]:focus {
  outline-offset: 2px;
  outline: 4px dashed var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 50%, #fff);
}

[ngTab]:active,
[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  box-shadow: var(--retro-pressed-shadow);
  transform: translate(4px, 4px);
}

[ngTabPanel] {
  flex: 1;
  font-size: 1.5rem;
  color: #000;
  background-color: #fff;
  padding: 1rem;
  margin-top: 1rem;
  min-height: 100px;
  display: grid;
  place-items: center;
  box-shadow: var(--retro-flat-shadow);
  background: linear-gradient(rgba(0, 0, 0, 0.3) 50%, rgba(0, 0, 0, 0.1) 50%);
  background-color: color-mix(in srgb, var(--bright-blue) 10%, #fff);
  background-size: 100% 4px;
}

[ngTabPanel]:focus {
  outline-offset: 4px;
  outline: 4px dashed var(--bright-blue);
}

[ngTabPanel][inert] {
  display: none;
}

برای فعال کردن این behavior، [selectionMode]="'follow'" را روی tab list تنظیم کنید.

Manual activation

در manual activation، arrow keyها focus را بین tabها جابه‌جا می‌کنند بدون اینکه tab selected تغییر کند. کاربران Space یا Enter را فشار می‌دهند تا tab دارای focus را activate کنند.

ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs>
  <div ngTabList selectionMode="explicit" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  border-radius: 0.5rem;
  border: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTabList] {
  padding: 0;
  display: flex;
  list-style: none;
  position: relative;
  border-bottom: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 0.75rem 0;
  cursor: pointer;
  text-align: center;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
}

[ngTab][aria-selected='false']:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  border-top-left-radius: 0.5rem;
  border-top-right-radius: 0.5rem;
}

.sliding-window {
  width: 300%;
  display: flex;
  transition: all 0.2s ease-in-out;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  min-height: 100px;
  flex: 1;
  border-bottom-right-radius: 0.5rem;
  border-bottom-left-radius: 0.5rem;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="material-tabs">
  <div ngTabList selectionMode="explicit" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
    <div class="bottom-border"></div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  border-radius: 0.5rem;
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTabList] {
  padding: 0;
  display: flex;
  list-style: none;
  position: relative;
  border-bottom: 1px solid color-mix(in srgb, var(--primary-contrast) 20%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem 0;
  cursor: pointer;
  text-align: center;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
}

[ngTab]:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--bright-blue);
}

.bottom-border {
  position: absolute;
  pointer-events: none;
  left: 0;
  bottom: 0;
  height: 3px;
  width: calc(100% / 3);
  background-color: var(--bright-blue);
  transition: all 0.2s ease-in-out;
  transform: translateX(0%);
  border-top-left-radius: 2px;
  border-top-right-radius: 2px;
}

[ngTab]:nth-child(1)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(0%);
}
[ngTab]:nth-child(2)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(100%);
}
[ngTab]:nth-child(3)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(200%);
}

.sliding-window {
  width: 300%;
  display: flex;
  transition: all 0.2s ease-in-out;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  min-height: 100px;
  flex: 1;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="retro-tabs">
  <div ngTabList selectionMode="explicit" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes">Reviews</div>
  </div>

  <div ngTabPanel value="movie">
    <ng-template ngTabContent>Line 1</ng-template>
  </div>

  <div ngTabPanel value="theatres">
    <ng-template ngTabContent>Line 2</ng-template>
  </div>

  <div ngTabPanel value="showtimes">
    <ng-template ngTabContent>Line 3</ng-template>
  </div>
</div>
css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;

  font-family: 'Press Start 2P';
  --retro-button-color: var(--bright-blue);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--quaternary-contrast),
    0px 4px 0px 0px var(--quaternary-contrast), -4px 0px 0px 0px var(--quaternary-contrast),
    0px -4px 0px 0px var(--quaternary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--quaternary-contrast), 0px 4px 0px 0px var(--quaternary-contrast),
    -4px 0px 0px 0px var(--quaternary-contrast), 0px -4px 0px 0px var(--quaternary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--quaternary-contrast),
    0px 4px 0px 0px var(--quaternary-contrast), -4px 0px 0px 0px var(--quaternary-contrast),
    0px -4px 0px 0px var(--quaternary-contrast), 8px 8px 0px 0px var(--quaternary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--quaternary-contrast),
    0px 4px 0px 0px var(--quaternary-contrast), -4px 0px 0px 0px var(--quaternary-contrast),
    0px -4px 0px 0px var(--quaternary-contrast), 0px 0px 0px 0px var(--quaternary-contrast);
}

[ngTabs] {
  width: 600px;
}

[ngTabList] {
  gap: 1rem;
  display: flex;
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem;
  cursor: pointer;
  text-align: center;
  color: #000;
  background-color: #fff;
  box-shadow: var(--retro-clickable-shadow);
  transition:
    transform 0.1s,
    box-shadow 0.1s;
}

[ngTab]:focus,
[ngTab]:hover {
  transform: translate(1px, 1px);
}

[ngTab]:focus {
  outline-offset: 2px;
  outline: 4px dashed var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 50%, #fff);
}

[ngTab]:active,
[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  box-shadow: var(--retro-pressed-shadow);
  transform: translate(4px, 4px);
}

[ngTabPanel] {
  flex: 1;
  font-size: 1.5rem;
  color: #000;
  background-color: #fff;
  padding: 1rem;
  margin-top: 1rem;
  min-height: 100px;
  display: grid;
  place-items: center;
  box-shadow: var(--retro-flat-shadow);
  background: linear-gradient(rgba(0, 0, 0, 0.3) 50%, rgba(0, 0, 0, 0.1) 50%);
  background-color: color-mix(in srgb, var(--bright-blue) 10%, #fff);
  background-size: 100% 4px;
}

[ngTabPanel]:focus {
  outline-offset: 4px;
  outline: 4px dashed var(--bright-blue);
}

[ngTabPanel][inert] {
  display: none;
}

برای content panelهای سنگین از [selectionMode]="'explicit'" استفاده کنید تا از rendering غیرضروری جلوگیری شود.

Vertical tabs

برای interfaceهایی مثل settings panel یا navigation sidebar، tabها را عمودی بچینید.

ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs>
  <div ngTabList orientation="vertical" selectedTab="movie">
    <div ngTab value="movie">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true">videocam</span>
    </div>
    <div ngTab value="theatres">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true"
        >theater_comedy</span
      >
    </div>
    <div ngTab value="showtimes">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true">reviews</span>
    </div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  height: 200px;
  border-radius: 0.5rem;
  border: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
  display: flex;
}

[ngTabList] {
  padding: 0;
  display: flex;
  flex-direction: column;
  list-style: none;
  position: relative;
  border-right: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  width: 80px;
  cursor: pointer;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
  display: grid;
  place-items: center;
}

[ngTab][aria-selected='false']:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  border-top-left-radius: 0.5rem;
  border-bottom-left-radius: 0.5rem;
}

.sliding-window {
  height: 300%;
  display: flex;
  flex-direction: column;
  transition: all 0.2s ease-in-out;
  flex: 1;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateY(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateY(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateY(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  flex: 1;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs>
  <div ngTabList class="material-tabs" orientation="vertical" selectedTab="movie">
    <div ngTab value="movie">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true">videocam</span>
    </div>
    <div ngTab value="theatres">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true"
        >theater_comedy</span
      >
    </div>
    <div ngTab value="showtimes">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true">reviews</span>
    </div>
    <div class="bottom-border"></div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  font-family: var(--inter-font);
  display: flex;;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  height: 200px;
  border-radius: 0.5rem;
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
  display: flex;
}

[ngTabList] {
  padding: 0;
  display: flex;
  flex-direction: column;
  list-style: none;
  position: relative;
  border-right: 1px solid color-mix(in srgb, var(--primary-contrast) 20%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  width: 80px;
  cursor: pointer;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
  display: grid;
  place-items: center;
}

[ngTab]:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--bright-blue);
}

.bottom-border {
  position: absolute;
  pointer-events: none;
  top: 0;
  right: 0;
  width: 3px;
  height: calc(100% / 3);
  background-color: var(--bright-blue);
  transition: all 0.2s ease-in-out;
  transform: translateY(0%);
  border-top-right-radius: 2px;
  border-bottom-right-radius: 2px;
}

[ngTab]:nth-child(1)[aria-selected='true'] ~ .bottom-border {
  transform: translateY(0%);
}
[ngTab]:nth-child(2)[aria-selected='true'] ~ .bottom-border {
  transform: translateY(100%);
}
[ngTab]:nth-child(3)[aria-selected='true'] ~ .bottom-border {
  transform: translateY(200%);
}

.sliding-window {
  height: 300%;
  display: flex;
  flex-direction: column;
  transition: all 0.2s ease-in-out;
  flex: 1;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateY(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateY(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateY(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  flex: 1;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="retro-tabs">
  <div ngTabList orientation="vertical" selectedTab="movie">
    <div ngTab value="movie">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true">videocam</span>
    </div>
    <div ngTab value="theatres">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true"
        >theater_comedy</span
      >
    </div>
    <div ngTab value="showtimes">
      <span class="material-symbols-outlined" translate="no" aria-hidden="true">reviews</span>
    </div>
  </div>

  <div ngTabPanel [preserveContent]="true" value="movie">
    <ng-template ngTabContent>Line 1</ng-template>
  </div>

  <div ngTabPanel [preserveContent]="true" value="theatres">
    <ng-template ngTabContent>Line 2</ng-template>
  </div>

  <div ngTabPanel [preserveContent]="true" value="showtimes">
    <ng-template ngTabContent>Line 3</ng-template>
  </div>
</div>
css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;

  font-family: 'Press Start 2P';
  --retro-button-color: var(--bright-blue);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[ngTabs] {
  width: 500px;
  display: flex;
}

[ngTabList] {
  gap: 1rem;
  display: flex;
  flex-direction: column;
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem 1.5rem;
  cursor: pointer;
  text-align: center;
  color: #000;
  background-color: #fff;
  box-shadow: var(--retro-clickable-shadow);
  transition:
    transform 0.1s,
    box-shadow 0.1s;
}

[ngTab]:focus,
[ngTab]:hover {
  transform: translate(1px, 1px);
}

[ngTab]:focus {
  outline-offset: 2px;
  outline: 4px dashed var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 50%, #fff);
}

[ngTab]:active,
[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  box-shadow: var(--retro-pressed-shadow);
  transform: translate(4px, 4px);
}

[ngTabPanel] {
  flex: 1;
  font-size: 1.5rem;
  color: #000;
  background-color: #fff;
  padding: 1rem;
  margin-left: 1rem;
  min-width: 100px;
  display: grid;
  place-items: center;
  box-shadow: var(--retro-flat-shadow);
  background: linear-gradient(rgba(0, 0, 0, 0.3) 50%, rgba(0, 0, 0, 0.1) 50%);
  background-color: color-mix(in srgb, var(--bright-blue) 10%, #fff);
  background-size: 100% 4px;
}

[ngTabPanel]:focus {
  outline-offset: 4px;
  outline: 4px dashed var(--bright-blue);
}

[ngTabPanel][inert] {
  display: none;
}

[orientation]="'vertical'" را روی tab list تنظیم کنید. navigation به arrow keyهای بالا/پایین تغییر می‌کند.

Lazy content rendering

از directive مربوط به ngTabContent روی یک ng-template استفاده کنید تا rendering مربوط به tab panelها تا اولین نمایششان به تأخیر بیفتد.

html
<div ngTabs>
  <ul ngTabList [(selectedTab)]="selectedTab">
    <li ngTab value="tab1">Tab 1</li>
    <li ngTab value="tab2">Tab 2</li>
  </ul>

  <div ngTabPanel value="tab1">
    <ng-template ngTabContent>
      <!-- This content only renders when Tab 1 is first shown -->
      <app-heavy-component />
    </ng-template>
  </div>

  <div ngTabPanel value="tab2">
    <ng-template ngTabContent>
      <!-- This content only renders when Tab 2 is first shown -->
      <app-another-component />
    </ng-template>
  </div>
</div>

به صورت پیش‌فرض، پس از hidden شدن panel، content در DOM باقی می‌ماند. برای حذف content هنگام deactivate شدن panel، [preserveContent]="false" را تنظیم کنید.

Disabled tabs

tabهای مشخص را disabled کنید تا از user interaction جلوگیری شود. کنترل کنید tabهای disabled بتوانند keyboard focus دریافت کنند یا نه.

ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs>
  <div ngTabList selectionMode="explicit" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes" disabled>Reviews</div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  border-radius: 0.5rem;
  border: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTabList] {
  padding: 0;
  display: flex;
  list-style: none;
  position: relative;
  border-bottom: 1px solid color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 0.75rem 0;
  cursor: pointer;
  text-align: center;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
}

[ngTab][aria-disabled='true'] {
  cursor: default;
  color: color-mix(in srgb, var(--primary-contrast) 30%, transparent);
  background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}

[ngTab][aria-selected='false']:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab][aria-disabled='false'][aria-selected='false']:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  border-top-left-radius: 0.5rem;
  border-top-right-radius: 0.5rem;
}

.sliding-window {
  width: 300%;
  display: flex;
  transition: all 0.2s ease-in-out;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  min-height: 100px;
  flex: 1;
  border-bottom-right-radius: 0.5rem;
  border-bottom-left-radius: 0.5rem;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="material-tabs">
  <div ngTabList selectionMode="explicit" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes" disabled>Reviews</div>
    <div class="bottom-border"></div>
  </div>

  <div class="sliding-window">
    <div ngTabPanel [preserveContent]="true" value="movie">
      <ng-template ngTabContent>Panel 1</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="theatres">
      <ng-template ngTabContent>Panel 2</ng-template>
    </div>

    <div ngTabPanel [preserveContent]="true" value="showtimes">
      <ng-template ngTabContent>Panel 3</ng-template>
    </div>
  </div>
</div>
css
:host {
  font-family: var(--inter-font);
  display: flex;
  justify-content: center;
}

[ngTabs] {
  overflow: hidden;
  width: 600px;
  border-radius: 0.5rem;
  background-color: color-mix(in srgb, var(--bright-blue) 5%, transparent);
}

[ngTabList] {
  padding: 0;
  display: flex;
  list-style: none;
  position: relative;
  border-bottom: 1px solid color-mix(in srgb, var(--primary-contrast) 20%, transparent);
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem 0;
  cursor: pointer;
  text-align: center;
  color: color-mix(in srgb, var(--primary-contrast) 60%, transparent);
}

[ngTab][aria-disabled='true'] {
  cursor: default;
  color: color-mix(in srgb, var(--primary-contrast) 30%, transparent);
  background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}

[ngTab]:focus {
  outline-offset: -8px;
  border-radius: 0.7rem;
  outline: 2px solid var(--bright-blue);
}

[ngTab]:hover {
  background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}

[ngTab][aria-selected='true'] {
  color: var(--bright-blue);
}

.bottom-border {
  position: absolute;
  pointer-events: none;
  left: 0;
  bottom: 0;
  height: 3px;
  width: calc(100% / 3);
  background-color: var(--bright-blue);
  transition: all 0.2s ease-in-out;
  transform: translateX(0%);
  border-top-left-radius: 2px;
  border-top-right-radius: 2px;
}

[ngTab]:nth-child(1)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(0%);
}
[ngTab]:nth-child(2)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(100%);
}
[ngTab]:nth-child(3)[aria-selected='true'] ~ .bottom-border {
  transform: translateX(200%);
}

.sliding-window {
  width: 300%;
  display: flex;
  transition: all 0.2s ease-in-out;
}

[ngTabList]:has([ngTab]:nth-child(1)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(0%);
}
[ngTabList]:has([ngTab]:nth-child(2)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-33.333%);
}
[ngTabList]:has([ngTab]:nth-child(3)[aria-selected='true']) ~ .sliding-window {
  transform: translateX(-66.666%);
}

[ngTabPanel] {
  display: grid;
  place-items: center;
  padding: 1rem;
  min-height: 100px;
  flex: 1;
}

[ngTabPanel]:focus {
  outline-offset: -4px;
  border-radius: 0.5rem;
  outline: 2px solid var(--bright-blue);
}
ts
import {Component} from '@angular/core';
import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [TabList, Tab, Tabs, TabPanel, TabContent],
})
export class App {}
html
<div ngTabs class="retro-tabs">
  <div ngTabList selectionMode="explicit" selectedTab="movie">
    <div ngTab value="movie">Movie</div>
    <div ngTab value="theatres">Cast</div>
    <div ngTab value="showtimes" disabled>Reviews</div>
  </div>

  <div ngTabPanel value="movie">
    <ng-template ngTabContent>Line 1</ng-template>
  </div>

  <div ngTabPanel value="theatres">
    <ng-template ngTabContent>Line 2</ng-template>
  </div>

  <div ngTabPanel value="showtimes">
    <ng-template ngTabContent>Line 3</ng-template>
  </div>
</div>
css
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;

  font-family: 'Press Start 2P';
  --retro-button-color: var(--bright-blue);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[ngTabs] {
  width: 600px;
}

[ngTabList] {
  gap: 1rem;
  display: flex;
}

[ngTab] {
  flex: 1;
  outline: none;
  padding: 1rem;
  cursor: pointer;
  text-align: center;
  color: #000;
  background-color: #fff;
  box-shadow: var(--retro-clickable-shadow);
  transition:
    transform 0.1s,
    box-shadow 0.1s;
}

[ngTab][aria-disabled='true'] {
  cursor: default;
  color: color-mix(in srgb, #000 30%, transparent);
}

[ngTab][aria-disabled='false'][aria-selected='false']:focus,
[ngTab][aria-disabled='false'][aria-selected='false']:hover {
  transform: translate(1px, 1px);
}

[ngTab]:focus {
  outline-offset: 2px;
  outline: 4px dashed var(--bright-blue);
}

[ngTab][aria-disabled='false'][aria-selected='false']:hover {
  background-color: color-mix(in srgb, var(--bright-blue) 50%, #fff);
}

[ngTab][aria-disabled='false']:active,
[ngTab][aria-selected='true'] {
  color: var(--page-background);
  background-color: var(--bright-blue);
  box-shadow: var(--retro-pressed-shadow);
  transform: translate(4px, 4px);
}

[ngTabPanel] {
  flex: 1;
  font-size: 1.5rem;
  color: #000;
  background-color: #fff;
  padding: 1rem;
  margin-top: 1rem;
  min-height: 100px;
  display: grid;
  place-items: center;
  box-shadow: var(--retro-flat-shadow);
  background: linear-gradient(rgba(0, 0, 0, 0.3) 50%, rgba(0, 0, 0, 0.1) 50%);
  background-color: color-mix(in srgb, var(--bright-blue) 10%, #fff);
  background-size: 100% 4px;
}

[ngTabPanel]:focus {
  outline-offset: 4px;
  outline: 4px dashed var(--bright-blue);
}

[ngTabPanel][inert] {
  display: none;
}

وقتی [softDisabled]="true" روی tab list باشد، tabهای disabled می‌توانند focus دریافت کنند اما activate نمی‌شوند. وقتی [softDisabled]="false" باشد، tabهای disabled هنگام keyboard navigation skip می‌شوند.

Testing

Angular Aria برای testing کامپوننت‌های tabs، component harness ارائه می‌کند. این نمونه نحوه استفاده از harnessها را در یک component test نشان می‌دهد:

ts
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {ComponentHarness, HarnessLoader} from '@angular/cdk/testing';
import {TabsHarness} from '@angular/aria/tabs/testing';
import {MyTabsComponent} from './my-tabs'; // Your component

// A simple harness to help query content inside the tab panel
class TestContentHarness extends ComponentHarness {
  static hostSelector = '.test-content';
  async getText(): Promise<string> {
    return (await this.host()).text();
  }
}

describe('MyTabsComponent', () => {
  let fixture: ComponentFixture<MyTabsComponent>;
  let loader: HarnessLoader;

  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [MyTabsComponent],
    });

    fixture = TestBed.createComponent(MyTabsComponent);
    await fixture.whenStable();
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  it('should switch tabs and scope panel queries', async () => {
    const tabs = await loader.getHarness(TabsHarness);

    // Get all tabs
    const tabItems = await tabs.getTabs();
    expect(tabItems.length).toBe(3);

    // Verify initial selection
    expect(await tabItems[0].isSelected()).toBe(true);
    expect(await tabItems[1].isSelected()).toBe(false);

    // Query content inside the active tab panel
    // TabHarness automatically scopes queries to its associated panel
    const content = await tabItems[0].getHarness(TestContentHarness);
    expect(await content.getText()).toBe('Content 1');

    // Switch to the second tab
    await tabItems[1].select();

    // Verify selection updated
    expect(await tabItems[0].isSelected()).toBe(false);
    expect(await tabItems[1].isSelected()).toBe(true);
  });
});

API reference

برای مستندات دقیق API، referenceهای API زیر را بررسی کنید: