Вкладки
Обзор
Вкладки отображают разделы содержимого, расположенные слоями, при этом одновременно видна только одна панель. Пользователи переключаются между панелями, нажимая кнопки вкладок или используя клавиши со стрелками для навигации по списку вкладок.
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 {}
<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>
: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);
}
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 {}
<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>
: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);
}
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 {}
<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>
@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;
}
Использование
Вкладки хорошо подходят для организации связанного содержимого в отдельные разделы, между которыми пользователи могут переключаться.
Используйте вкладки, когда нужно:
- Организовать связанное содержимое в отдельные разделы
- Создать панели настроек с несколькими категориями
- Создать документацию с несколькими темами
- Реализовать панели мониторинга с разными представлениями
- Показать содержимое, для просмотра которого пользователям нужно переключаться между контекстами
Не используйте вкладки, если нужно:
- Создать последовательную форму или мастер (используйте шаблон пошагового перехода)
- Перемещаться между страницами (используйте навигацию маршрутизатора)
- Показать один раздел содержимого (вкладки не нужны)
- Разместить больше 7–8 вкладок (рассмотрите другой макет)
Возможности
- Режимы выбора — вкладки активируются автоматически при получении фокуса или требуют ручной активации
- Навигация с клавиатуры — клавиши со стрелками, Home и End для удобной навигации по вкладкам
- Ориентация — горизонтальное или вертикальное расположение списка вкладок
- Отложенная загрузка содержимого — панели вкладок отображаются только после первой активации
- Отключенные вкладки — отключение отдельных вкладок с управлением фокусом
- Режимы фокуса — стратегии управления фокусом roving tabindex или activedescendant
- Поддержка RTL — навигация для языков с письмом справа налево
Примеры
Выбор следует за фокусом
Когда выбор следует за фокусом, вкладки активируются сразу при перемещении по ним с помощью клавиш со стрелками. Это обеспечивает мгновенную обратную связь и хорошо подходит для легковесного содержимого.
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 {}
<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>
: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);
}
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 {}
<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>
: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);
}
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 {}
<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>
@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;
}
Установите [selectionMode]="'follow'" для списка вкладок, чтобы включить это поведение.
Ручная активация
При ручной активации клавиши со стрелками перемещают фокус между вкладками, не меняя выбранную вкладку. Чтобы активировать вкладку в фокусе, пользователи нажимают клавишу пробела или Enter.
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 {}
<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>
: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);
}
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 {}
<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>
: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);
}
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 {}
<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>
@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;
}
Используйте [selectionMode]="'explicit'" для панелей с ресурсоемким содержимым, чтобы избежать ненужного отображения.
Вертикальные вкладки
Расположите вкладки вертикально, например, в панелях настроек или боковых панелях навигации.
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 {}
<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>
@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);
}
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 {}
<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>
@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);
}
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 {}
<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>
@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'" для списка вкладок. Для навигации используются клавиши со стрелками вверх и вниз.
Отложенное отображение содержимого
Используйте директиву ngTabContent для элемента ng-template, чтобы отложить отображение панелей вкладок до их первого показа.
<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>
По умолчанию содержимое остается в DOM после скрытия панели. Установите [preserveContent]="false", чтобы удалять содержимое при деактивации панели.
Отключенные вкладки
Отключите определенные вкладки, чтобы запретить взаимодействие с ними. Настройте, могут ли отключенные вкладки получать фокус с клавиатуры.
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 {}
<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>
: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);
}
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 {}
<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>
: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);
}
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 {}
<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>
@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", отключенные вкладки могут получать фокус, но не могут активироваться. Если установлено [softDisabled]="false", отключенные вкладки пропускаются при навигации с клавиатуры.
Тестирование
Angular Aria предоставляет компонентные harness для тестирования компонентов вкладок. Вот пример использования harness в тесте компонента:
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
Tabs
Директива-контейнер, которая координирует списки вкладок и панели.
У этой директивы нет входных или выходных параметров. Она служит корневым контейнером для директив ngTabList, ngTab и ngTabPanel.
TabList
Контейнер для кнопок вкладок, управляющий выбором и навигацией с клавиатуры.
Входные параметры
| Свойство | Тип | Значение по умолчанию | Описание |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'horizontal' | Направление расположения списка вкладок |
wrap | boolean | true | Определяет, циклически ли перемещается фокус с последней вкладки на первую |
softDisabled | boolean | true | Если установлено true, отключенные вкладки могут получать фокус, но не могут активироваться |
selectionMode | 'follow' | 'explicit' | 'follow' | Определяет, активируются ли вкладки при получении фокуса или требуют явной активации |
focusMode | 'roving' | 'activedescendant' | 'roving' | Стратегия управления фокусом |
selectedTab | any | — | Значение текущей выбранной вкладки (поддерживает двустороннюю привязку) |
Tab
Отдельная кнопка вкладки.
Входные параметры
| Свойство | Тип | Значение по умолчанию | Описание |
|---|---|---|---|
value | any | — | Обязательно. Уникальное значение этой вкладки |
disabled | boolean | false | Отключает эту вкладку |
Сигналы
| Свойство | Тип | Описание |
|---|---|---|
selected | Signal<boolean> | Определяет, выбрана ли сейчас эта вкладка |
active | Signal<boolean> | Определяет, находится ли сейчас фокус на этой вкладке |
TabPanel
Панель содержимого, связанная с вкладкой.
Входные параметры
| Свойство | Тип | Значение по умолчанию | Описание |
|---|---|---|---|
value | any | — |
Обязательно. Должно совпадать со значением value связанной вкладки |
preserveContent | boolean | true | Определяет, следует ли сохранять содержимое панели в DOM после деактивации |
Сигналы
| Свойство | Тип | Описание |
|---|---|---|
visible | Signal<boolean> | Определяет, видна ли сейчас панель |
TabContent
Структурная директива для отложенного отображения содержимого панели вкладки.
У этой директивы нет входных или выходных параметров и методов. Примените ее к элементу ng-template внутри панели вкладки:
<div ngTabPanel value="tab1">
<ng-template ngTabContent>
<!-- Content here is lazily rendered -->
</ng-template>
</div>
Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/aria/tabs