# Tabs

- Entry point: `@koobiq/components/tabs`
- NgModule: `KbqTabsModule`
- Page: https://koobiq.io/en/components/tabs/overview

## Overview

Tabs divide content into groups and allow you to switch between them without reloading the page.

**Example: Tabs overview** (`tabs-overview`)

```ts tabs-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-overview-example',
    imports: [KbqTabsModule],
    template: `
        <kbq-tab-group>
            <kbq-tab label="BruteForce">BruteForce tab content</kbq-tab>
            <kbq-tab label="Complex attack">Complex attack tab content</kbq-tab>
            <kbq-tab label="DDoS">DDoS tab content</kbq-tab>
            <kbq-tab label="DoS">DoS tab content</kbq-tab>
        </kbq-tab-group>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsOverviewExample {}
```

### States

#### Vertical tabs

You can place a long list of tabs vertically so all elements can be visible without scrolling. To enable the function, use the `vertical` attribute.

**Example: Tabs vertical** (`tabs-vertical`)

```ts tabs-vertical-example.ts
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTabsModule } from '@koobiq/components/tabs';
import { KbqTitleModule } from '@koobiq/components/title';

@Component({
    selector: 'tabs-vertical-example',
    imports: [
        KbqTabsModule,
        KbqIconModule,
        KbqTitleModule
    ],
    template: `
        <div class="example-tabs-vertical">
            <kbq-tab-group vertical>
                @for (tab of tabs; track tab) {
                    <kbq-tab>
                        <ng-template kbq-tab-label>
                            <i kbq-icon="kbq-bug_16"></i>
                            <div class="example-tab-label" kbq-title>{{ tab }}</div>
                        </ng-template>
                        <p>
                            In computing [{{ $index }}], a denial-of-service attack (DoS attack) is a cyber-attack in
                            which the perpetrator seeks to make a machine or network resource unavailable to its
                            intended users by temporarily or indefinitely disrupting services of a host connected to a
                            network. Denial of service is typically accomplished by flooding the targeted machine or
                            resource with superfluous requests in an attempt to overload systems and prevent some or all
                            legitimate requests from being fulfilled. The range of attacks varies widely, spanning from
                            inundating a server with millions of requests to slow its performance, overwhelming a server
                            with a substantial amount of invalid data, to submitting requests with an illegitimate IP
                            address.
                        </p>
                        <p>
                            In a distributed denial-of-service attack [{{ $index }}] (DDoS attack), the incoming traffic
                            flooding the victim originates from many different sources. More sophisticated strategies
                            are required to mitigate this type of attack; simply attempting to block a single source is
                            insufficient as there are multiple sources. A DoS or DDoS attack is analogous to a group of
                            people crowding the entry door of a shop, making it hard for legitimate customers to enter,
                            thus disrupting trade and losing the business money. Criminal perpetrators of DoS attacks
                            often target sites or services hosted on high-profile web servers such as banks or credit
                            card payment gateways. Revenge and blackmail, as well as hacktivism, can motivate these
                            attacks.
                        </p>
                    </kbq-tab>
                }
            </kbq-tab-group>
        </div>
    `,
    styleUrls: ['tabs-vertical-example.css'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    encapsulation: ViewEncapsulation.None
})
export class TabsVerticalExample {
    readonly tabs = [
        'Complex Attack',
        'HIPS alert',
        'IDS/IPS Alert',
        'Vulnerability Exploitation'
    ];
}
```

```css tabs-vertical-example.css
.example-tabs-vertical .kbq-tab-group_vertical {
    height: 220px;
}

.example-tabs-vertical .kbq-tab-header {
    max-width: 140px;
}

.example-tabs-vertical .kbq-tab-body__wrapper {
    padding-left: var(--kbq-size-xl);
}

.example-tabs-vertical .example-tab-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
```

You can use untitled vertical tabs. In this case, add a tooltip for icons to clarify the name of each section.

**Example: Tabs vertical icons** (`tabs-vertical-icons`)

```ts tabs-vertical-icons-example.ts
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { PopUpPlacements } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-vertical-icons-example',
    imports: [
        KbqTabsModule,
        KbqIconModule
    ],
    template: `
        <div class="example-tabs-vertical-icons">
            <kbq-tab-group vertical>
                <kbq-tab tooltipTitle="BruteForce" empty [tooltipPlacement]="PopUpPlacements.Left">
                    <ng-template kbqTabLabel iconOnly>
                        <i kbq-icon="kbq-bug_16"></i>
                    </ng-template>
                    <p>
                        In computing, a denial-of-service attack (DoS attack) is a cyber-attack in which the perpetrator
                        seeks to make a machine or network resource unavailable to its intended users by temporarily or
                        indefinitely disrupting services of a host connected to a network. Denial of service is
                        typically accomplished by flooding the targeted machine or resource with superfluous requests in
                        an attempt to overload systems and prevent some or all legitimate requests from being fulfilled.
                        The range of attacks varies widely, spanning from inundating a server with millions of requests
                        to slow its performance, overwhelming a server with a substantial amount of invalid data, to
                        submitting requests with an illegitimate IP address.
                    </p>
                    <p>
                        In a distributed denial-of-service attack (DDoS attack), the incoming traffic flooding the
                        victim originates from many different sources. More sophisticated strategies are required to
                        mitigate this type of attack; simply attempting to block a single source is insufficient as
                        there are multiple sources. A DoS or DDoS attack is analogous to a group of people crowding the
                        entry door of a shop, making it hard for legitimate customers to enter, thus disrupting trade
                        and losing the business money. Criminal perpetrators of DoS attacks often target sites or
                        services hosted on high-profile web servers such as banks or credit card payment gateways.
                        Revenge and blackmail, as well as hacktivism, can motivate these attacks.
                    </p>
                </kbq-tab>
                <kbq-tab tooltipTitle="Complex Attack" empty [tooltipPlacement]="PopUpPlacements.Left">
                    <ng-template kbqTabLabel iconOnly>
                        <i kbq-icon="kbq-bug_16"></i>
                    </ng-template>
                    <p>
                        In cryptography, a brute-force attack consists of an attacker submitting many passwords or
                        passphrases with the hope of eventually guessing correctly. The attacker systematically checks
                        all possible passwords and passphrases until the correct one is found. Alternatively, the
                        attacker can attempt to guess the key which is typically created from the password using a key
                        derivation function. This is known as an exhaustive key search. This approach doesn't depend on
                        intellectual tactics; rather, it relies on making several attempts.
                    </p>
                    <p>
                        A brute-force attack is a cryptanalytic attack that can, in theory, be used to attempt to
                        decrypt any encrypted data (except for data encrypted in an information-theoretically secure
                        manner). Such an attack might be used when it is not possible to take advantage of other
                        weaknesses in an encryption system (if any exist) that would make the task easier.
                    </p>
                </kbq-tab>
                <kbq-tab tooltipTitle="DDoS" empty [tooltipPlacement]="PopUpPlacements.Left">
                    <ng-template kbqTabLabel iconOnly>
                        <i kbq-icon="kbq-bug_16"></i>
                    </ng-template>
                    <p>
                        In computing, a denial-of-service attack (DoS attack) is a cyber-attack in which the perpetrator
                        seeks to make a machine or network resource unavailable to its intended users by temporarily or
                        indefinitely disrupting services of a host connected to a network. Denial of service is
                        typically accomplished by flooding the targeted machine or resource with superfluous requests in
                        an attempt to overload systems and prevent some or all legitimate requests from being fulfilled.
                        The range of attacks varies widely, spanning from inundating a server with millions of requests
                        to slow its performance, overwhelming a server with a substantial amount of invalid data, to
                        submitting requests with an illegitimate IP address.
                    </p>
                    <p>
                        In a distributed denial-of-service attack (DDoS attack), the incoming traffic flooding the
                        victim originates from many different sources. More sophisticated strategies are required to
                        mitigate this type of attack; simply attempting to block a single source is insufficient as
                        there are multiple sources. A DoS or DDoS attack is analogous to a group of people crowding the
                        entry door of a shop, making it hard for legitimate customers to enter, thus disrupting trade
                        and losing the business money. Criminal perpetrators of DoS attacks often target sites or
                        services hosted on high-profile web servers such as banks or credit card payment gateways.
                        Revenge and blackmail, as well as hacktivism, can motivate these attacks.
                    </p>
                </kbq-tab>
                <kbq-tab tooltipTitle="DoS" empty [tooltipPlacement]="PopUpPlacements.Left">
                    <ng-template kbqTabLabel iconOnly>
                        <i kbq-icon="kbq-bug_16"></i>
                    </ng-template>
                    <p>
                        In cryptography, a brute-force attack consists of an attacker submitting many passwords or
                        passphrases with the hope of eventually guessing correctly. The attacker systematically checks
                        all possible passwords and passphrases until the correct one is found. Alternatively, the
                        attacker can attempt to guess the key which is typically created from the password using a key
                        derivation function. This is known as an exhaustive key search. This approach doesn't depend on
                        intellectual tactics; rather, it relies on making several attempts.
                    </p>
                    <p>
                        A brute-force attack is a cryptanalytic attack that can, in theory, be used to attempt to
                        decrypt any encrypted data (except for data encrypted in an information-theoretically secure
                        manner). Such an attack might be used when it is not possible to take advantage of other
                        weaknesses in an encryption system (if any exist) that would make the task easier.
                    </p>
                </kbq-tab>
            </kbq-tab-group>
        </div>
    `,
    styleUrls: ['tabs-vertical-icons-example.css'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    encapsulation: ViewEncapsulation.None
})
export class TabsVerticalIconsExample {
    readonly PopUpPlacements = PopUpPlacements;
}
```

```css tabs-vertical-icons-example.css
.example-tabs-vertical-icons .kbq-tab-group_vertical {
    height: 220px;
}

.example-tabs-vertical-icons .kbq-tab-body__wrapper {
    padding-left: var(--kbq-size-xl);
}
```

#### Underlined tabs

Use the `underlined` attribute.

**Example: Tabs underlined** (`tabs-underlined`)

```ts tabs-underlined-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-underlined-example',
    imports: [KbqTabsModule, KbqIconModule],
    template: `
        <kbq-tab-group class="layout-margin-bottom-l" underlined>
            @for (tab of tabs; track tab) {
                <kbq-tab [label]="tab" />
            }
        </kbq-tab-group>

        <kbq-tab-group class="layout-margin-bottom-l" underlined>
            @for (tab of tabs; track tab) {
                <kbq-tab>
                    <ng-template kbqTabLabel>
                        <i kbq-icon="kbq-bug_16"></i>
                        {{ tab }}
                    </ng-template>
                </kbq-tab>
            }
        </kbq-tab-group>

        <kbq-tab-group underlined>
            @for (tab of tabs; track tab) {
                <kbq-tab>
                    <ng-template kbqTabLabel iconOnly>
                        <i kbq-icon="kbq-bug_16"></i>
                    </ng-template>
                </kbq-tab>
            }
        </kbq-tab-group>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsUnderlinedExample {
    readonly tabs = ['BruteForce', 'Complex Attack', 'DDoS', 'HIPS alert'];
}
```

If a tab contains only an icon without a label, add the `iconOnly` attribute to the `kbqTabLabel` directive - this sets a fixed size of 32×32px for the clickable area.

#### Disabled

A disabled tab can't be selected. It doesn't have interactive states.

**Example: Tabs disabled** (`tabs-disabled`)

```ts tabs-disabled-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-disabled-example',
    imports: [KbqTabsModule],
    template: `
        <kbq-tab-group class="layout-margin-bottom-m">
            <kbq-tab label="BruteForce" />
            <kbq-tab label="Complex attack" disabled />
            <kbq-tab label="DDoS" disabled />
            <kbq-tab label="DoS" />
        </kbq-tab-group>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsDisabledExample {}
```

#### Empty tab

To specify that a tab is empty, use the `empty` attribute. You can select such a tab, and it has interactive states.

**Example: Tabs empty label** (`tabs-empty-label`)

```ts tabs-empty-label-example.ts
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { PopUpPlacements } from '@koobiq/components/core';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-empty-label-example',
    imports: [KbqTabsModule],
    template: `
        <div class="example-tabs-empty-label">
            <kbq-tab-group>
                <kbq-tab label="BruteForce">
                    <div class="example-tabs-empty-label_tab-content">1005 incidents have been registered</div>
                </kbq-tab>
                <kbq-tab label="Complex Attack">
                    <p>
                        In cryptography, a brute-force attack consists of an attacker submitting many passwords or
                        passphrases with the hope of eventually guessing correctly. The attacker systematically checks
                        all possible passwords and passphrases until the correct one is found. Alternatively, the
                        attacker can attempt to guess the key which is typically created from the password using a key
                        derivation function. This is known as an exhaustive key search. This approach doesn't depend on
                        intellectual tactics; rather, it relies on making several attempts.
                    </p>
                    <p>
                        A brute-force attack is a cryptanalytic attack that can, in theory, be used to attempt to
                        decrypt any encrypted data (except for data encrypted in an information-theoretically secure
                        manner). Such an attack might be used when it is not possible to take advantage of other
                        weaknesses in an encryption system (if any exist) that would make the task easier.
                    </p>
                </kbq-tab>
                <kbq-tab label="DDoS" tooltipTitle="No incidents" empty [tooltipPlacement]="PopUpPlacements.Top">
                    <div class="example-tabs-empty-label_tab-content">No DDoS attacks have been registered</div>
                </kbq-tab>
                <kbq-tab label="DoS">
                    <p>
                        In computing, a denial-of-service attack (DoS attack) is a cyber-attack in which the perpetrator
                        seeks to make a machine or network resource unavailable to its intended users by temporarily or
                        indefinitely disrupting services of a host connected to a network. Denial of service is
                        typically accomplished by flooding the targeted machine or resource with superfluous requests in
                        an attempt to overload systems and prevent some or all legitimate requests from being fulfilled.
                        The range of attacks varies widely, spanning from inundating a server with millions of requests
                        to slow its performance, overwhelming a server with a substantial amount of invalid data, to
                        submitting requests with an illegitimate IP address.
                    </p>
                    <p>
                        In a distributed denial-of-service attack (DDoS attack), the incoming traffic flooding the
                        victim originates from many different sources. More sophisticated strategies are required to
                        mitigate this type of attack; simply attempting to block a single source is insufficient as
                        there are multiple sources. A DoS or DDoS attack is analogous to a group of people crowding the
                        entry door of a shop, making it hard for legitimate customers to enter, thus disrupting trade
                        and losing the business money. Criminal perpetrators of DoS attacks often target sites or
                        services hosted on high-profile web servers such as banks or credit card payment gateways.
                        Revenge and blackmail, as well as hacktivism, can motivate these attacks.
                    </p>
                </kbq-tab>
            </kbq-tab-group>
        </div>
    `,
    styleUrls: ['tabs-empty-label-example.css'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    encapsulation: ViewEncapsulation.None
})
export class TabsEmptyLabelExample {
    readonly PopUpPlacements = PopUpPlacements;
}
```

```css tabs-empty-label-example.css
.example-tabs-empty-label .example-tabs-empty-label_tab-content {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 200px;
}
```

Don't use empty tabs together with inactive tabs. They look similar, which may confuse users.

#### Scroll

Horizontal tabs don't wrap or shrink. If there isn't enough space, some tabs are hidden and scroll buttons appear at the edges. Tabs can also be scrolled horizontally on a touchpad, with the mouse wheel while holding Shift, or by dragging.

**Example: Tabs with scroll** (`tabs-with-scroll`)

```ts tabs-with-scroll-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-with-scroll-example',
    imports: [KbqTabsModule],
    template: `
        <div class="tabs-with-scroll-example">
            <kbq-tab-group>
                @for (tab of tabs; track tab) {
                    <kbq-tab [label]="tab" />
                }
            </kbq-tab-group>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsWithScrollExample {
    readonly tabs = [
        'BruteForce',
        'Complex Attack',
        'DDoS',
        'HIPS alert',
        'IDS/IPS Alert',
        'Zero-Day Exploit',
        'XSS',
        'Malware',
        'Ransomware',
        'Phishing'
    ];
}
```

Vertical tabs can be scrolled if they don't fit within the available height.

**Example: Tabs with scroll vertical** (`tabs-with-scroll-vertical`)

```ts tabs-with-scroll-vertical-example.ts
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-with-scroll-vertical-example',
    imports: [KbqTabsModule],
    template: `
        <div class="example-tabs-with-scroll-vertical">
            <kbq-tab-group vertical>
                @for (tab of tabs; track tab) {
                    <kbq-tab [label]="tab">{{ tab }} tab content</kbq-tab>
                }
            </kbq-tab-group>
        </div>
    `,
    styleUrls: ['tabs-with-scroll-vertical-example.css'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    encapsulation: ViewEncapsulation.None
})
export class TabsWithScrollVerticalExample {
    readonly tabs = [
        'BruteForce',
        'Complex Attack',
        'DDoS',
        'HIPS alert',
        'IDS/IPS Alert',
        'Zero-Day Exploit',
        'XSS',
        'Malware',
        'Ransomware',
        'Phishing'
    ];
}
```

```css tabs-with-scroll-vertical-example.css
.example-tabs-with-scroll-vertical .kbq-tab-group {
    height: 176px;
}

.example-tabs-with-scroll-vertical .kbq-tab-header {
    width: 200px;
}
```

#### Adjusting to the container width

By default, the width of horizontal tabs depends on the text inside them. If necessary, the panel with tabs can match the container width. For this, use the `kbq-stretch-tabs` attribute.

**Example: Tabs stretch** (`tabs-stretch`)

```ts tabs-stretch-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-stretch-example',
    imports: [KbqTabsModule],
    template: `
        <kbq-tab-group kbq-stretch-tabs>
            <kbq-tab label="BruteForce" />
            <kbq-tab label="Identity Theft" />
            <kbq-tab label="Spam Attack" />
        </kbq-tab-group>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsStretchExample {}
```

### Customizable tab design

You can configure how tabs look according to your product design using the `kbqTabLabel` directive.

**Example: Tabs custom label** (`tabs-custom-label`)

```ts tabs-custom-label-example.ts
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-custom-label-example',
    imports: [
        KbqTabsModule,
        KbqIconModule
    ],
    template: `
        <div class="example-tabs-custom-label">
            <kbq-tab-group underlined>
                <kbq-tab>
                    <ng-template kbq-tab-label>
                        <i class="example-tabs-custom-label__icon" kbq-icon="kbq-apple_24"></i>
                        <div>macOS</div>
                        <div class="example-tabs-custom-label__subtitle">14.5+</div>
                    </ng-template>
                </kbq-tab>
                <kbq-tab>
                    <ng-template kbq-tab-label>
                        <i class="example-tabs-custom-label__icon" kbq-icon="kbq-windows_24"></i>
                        <div>Windows</div>
                        <div class="example-tabs-custom-label__subtitle">XP+</div>
                    </ng-template>
                </kbq-tab>
                <kbq-tab>
                    <ng-template kbq-tab-label>
                        <i class="example-tabs-custom-label__icon" kbq-icon="kbq-linux_24"></i>
                        <div>Linux</div>
                        <div class="example-tabs-custom-label__subtitle">Ubuntu 10+</div>
                    </ng-template>
                </kbq-tab>
                <kbq-tab>
                    <ng-template kbq-tab-label>
                        <i class="example-tabs-custom-label__icon" kbq-icon="kbq-bsd_24"></i>
                        <div>FreeBSD</div>
                        <div class="example-tabs-custom-label__subtitle">14.1+</div>
                    </ng-template>
                </kbq-tab>
            </kbq-tab-group>
        </div>
    `,
    styleUrls: ['tabs-custom-label-example.css'],
    changeDetection: ChangeDetectionStrategy.OnPush,
    encapsulation: ViewEncapsulation.None
})
export class TabsCustomLabelExample {}
```

```css tabs-custom-label-example.css
.example-tabs-custom-label {
    padding-bottom: var(--kbq-size-xxl);
}

.example-tabs-custom-label .example-tabs-custom-label__icon {
    margin-bottom: var(--kbq-size-xxs);
}

.example-tabs-custom-label .kbq-tab-label .kbq-tab-label__content {
    flex-direction: column;
    align-items: center;
    gap: 0;
}

.example-tabs-custom-label .example-tabs-custom-label__subtitle {
    font-size: var(--kbq-typography-text-compact-font-size);
    line-height: var(--kbq-typography-text-compact-line-height);
    font-weight: var(--kbq-typography-text-compact-font-weight);
    letter-spacing: var(--kbq-typography-text-compact-letter-spacing);
    color: var(--kbq-foreground-contrast-secondary);
}
```

### Tabs and navigation

Unlike `KbqTabGroup`, `KbqTabNavBar` is used for navigation between application pages or sections (using `routerLink`).

A nav bar is a navigation landmark by default. It only becomes a real tablist when you point it at the region it drives with `[tabNavPanel]` — see [Accessibility](#accessibility).

**Example: Tabs nav bar overview** (`tabs-nav-bar-overview`)

```ts tabs-nav-bar-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-nav-bar-overview-example',
    imports: [KbqTabsModule],
    template: `
        <nav kbqTabNavBar [tabNavPanel]="tabNavPanel">
            @for (link of links; track link) {
                <a kbqTabLink [active]="activeLink === link" (click)="activeLink = link">
                    {{ link }}
                </a>
            }
            <a disabled kbqTabLink>DoS</a>
        </nav>
        <main #tabNavPanel="kbqTabNavPanel" kbqTabNavPanel>
            Active link: {{ activeLink }}
            <!-- Perfect place for your <router-outlet /> -->
        </main>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsNavBarOverviewExample {
    readonly links = ['BruteForce', 'Complex attack', 'DDos'];
    activeLink = this.links[0];
}
```

### Creating tabs

Create and delete tabs like in a browser. The create button is located at the end of the list of tabs. When there is scrolling, the button moves to the right to remain visible.

**Example: Tabs add tab** (`tabs-add-tab`)

```ts tabs-add-tab-example.ts
import { afterNextRender, ChangeDetectionStrategy, Component, inject, Injector, viewChildren } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTabLink, KbqTabsModule } from '@koobiq/components/tabs';
import { KbqToolTipModule } from '@koobiq/components/tooltip';

@Component({
    selector: 'tabs-add-tab-example',
    imports: [KbqTabsModule, KbqButtonModule, KbqIconModule, KbqToolTipModule],
    template: `
        <div class="example-tabs-add-tab__bar">
            <nav kbqTabNavBar class="example-tab-nav-bar" [tabNavPanel]="tabNavPanel">
                @for (tab of tabs; track tab) {
                    <a kbqTabLink [active]="activeTab === tab" (click)="activeTab = tab">
                        {{ tab }}
                        @if (activeTab === tab) {
                            <div class="example-tab-close">
                                <button
                                    color="contrast-fade"
                                    kbq-icon-button="kbq-xmark-s_16"
                                    kbqTooltip="Remove tab"
                                    aria-label="Remove tab"
                                    (click)="removeTab(tab, $event)"
                                    (keydown)="$event.stopPropagation()"
                                ></button>
                            </div>
                        }
                    </a>
                }
            </nav>
            <button
                color="contrast"
                kbqStyle="transparent"
                kbq-button
                aria-label="Add"
                kbqTooltip="Add tab"
                (click)="addTab()"
            >
                <i kbq-icon="kbq-plus_16"></i>
            </button>
        </div>

        <div #tabNavPanel="kbqTabNavPanel" kbqTabNavPanel class="example-tabs-add-tab__content">
            {{ activeTab }} content
        </div>
    `,
    styleUrls: ['./tabs-add-tab-example.css'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsAddTabExample {
    private readonly injector = inject(Injector);
    private readonly tabLinks = viewChildren(KbqTabLink);

    protected tabs = ['BruteForce', 'Complex Attack', 'DDoS', 'HIPS alert'];
    protected activeTab = this.tabs[0];

    protected addTab(): void {
        const newTab = `Tab ${this.tabs.length + 1}`;

        this.tabs = [...this.tabs, newTab];
        this.activeTab = newTab;

        afterNextRender(() => this.tabLinks().at(-1)?.focus(), { injector: this.injector });
    }

    protected removeTab(tab: string, event: Event): void {
        event.stopPropagation();
        const index = this.tabs.indexOf(tab);

        this.tabs = this.tabs.filter((t) => t !== tab);

        if (this.tabs.length > 0) {
            this.activeTab = this.tabs[Math.max(0, index - 1)];
        }
    }
}
```

```css ./tabs-add-tab-example.css
:host {
    display: flex;
    flex-direction: column;
    --tab-nav-selected: var(--kbq-tabs-tab-item-filled-on-background-states-selected-background);
}

.example-tabs-add-tab__bar {
    display: flex;
    align-items: center;
}

.example-tabs-add-tab__content {
    padding-top: var(--kbq-size-m);
}

.example-tab-nav-bar {
    overflow: hidden;
}

.example-tab-close {
    position: absolute;
    right: calc(var(--kbq-tabs-size-tab-item-padding-horizontal) - var(--kbq-tabs-size-tab-item-focus-outline-width));
    top: 0;
    bottom: 0;
    width: 32px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    background: linear-gradient(var(--tab-nav-selected), var(--tab-nav-selected)), var(--kbq-background-bg);
    mask-image: linear-gradient(to right, transparent 0%, black 50%);
}
```

Vertical tabs

**Example: Tabs add tab vertical** (`tabs-add-tab-vertical`)

```ts tabs-add-tab-vertical-example.ts
import { afterNextRender, ChangeDetectionStrategy, Component, inject, Injector, viewChildren } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqScrollbarViewport } from '@koobiq/components/scrollbar';
import { KbqTabLink, KbqTabsModule } from '@koobiq/components/tabs';
import { KbqToolTipModule } from '@koobiq/components/tooltip';

@Component({
    selector: 'tabs-add-tab-vertical-example',
    imports: [KbqTabsModule, KbqButtonModule, KbqIconModule, KbqToolTipModule, KbqScrollbarViewport],
    template: `
        <div class="example-tab-nav-bar__wrapper">
            <nav kbqTabNavBar vertical class="example-tab-nav-bar" [tabNavPanel]="tabNavPanel">
                @for (tab of tabs; track tab) {
                    <a kbqTabLink class="example-tab-link" [active]="activeTab === tab" (click)="activeTab = tab">
                        {{ tab }}
                        @if (activeTab === tab) {
                            <div class="example-tab-close">
                                <button
                                    color="contrast-fade"
                                    kbq-icon-button="kbq-xmark-s_16"
                                    kbqTooltip="Remove tab"
                                    aria-label="Remove tab"
                                    (click)="removeTab(tab, $event)"
                                    (keydown)="$event.stopPropagation()"
                                ></button>
                            </div>
                        }
                    </a>
                }
            </nav>
            <button
                class="example-tab-add-button"
                color="contrast-fade"
                kbq-button
                aria-label="Add"
                kbqTooltip="Add tab"
                (click)="addTab()"
                (keydown)="$event.stopPropagation()"
            >
                <i kbq-icon="kbq-plus_16"></i>
            </button>
        </div>

        <div
            #tabNavPanel="kbqTabNavPanel"
            kbqScrollbarViewport
            kbqTabNavPanel
            class="example-tabs-add-tab-vertical__content"
        >
            <h2 [style.margin-top.px]="0">{{ activeTab }}</h2>
            <p>
                In cryptography, a brute-force attack consists of an attacker submitting many passwords or passphrases
                with the hope of eventually guessing correctly. The attacker systematically checks all possible
                passwords and passphrases until the correct one is found. Alternatively, the attacker can attempt to
                guess the key which is typically created from the password using a key derivation function. This is
                known as an exhaustive key search. This approach doesn't depend on intellectual tactics; rather, it
                relies on making several attempts.
            </p>
            <p>
                In cryptography, a brute-force attack consists of an attacker submitting many passwords or passphrases
                with the hope of eventually guessing correctly. The attacker systematically checks all possible
                passwords and passphrases until the correct one is found. Alternatively, the attacker can attempt to
                guess the key which is typically created from the password using a key derivation function. This is
                known as an exhaustive key search. This approach doesn't depend on intellectual tactics; rather, it
                relies on making several attempts.
            </p>
        </div>
    `,
    styleUrls: ['./tabs-add-tab-vertical-example.css'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsAddTabVerticalExample {
    private readonly injector = inject(Injector);
    private readonly tabLinks = viewChildren(KbqTabLink);

    protected tabs = ['BruteForce', 'Complex Attack', 'DDoS', 'HIPS alert'];
    protected activeTab = this.tabs[0];

    protected addTab(): void {
        const newTab = `Tab ${this.tabs.length + 1}`;

        this.tabs = [...this.tabs, newTab];
        this.activeTab = newTab;

        afterNextRender(() => this.tabLinks().at(-1)?.focus(), { injector: this.injector });
    }

    protected removeTab(tab: string, event: Event): void {
        event.stopPropagation();
        const index = this.tabs.indexOf(tab);

        this.tabs = this.tabs.filter((t) => t !== tab);

        if (this.tabs.length > 0) {
            this.activeTab = this.tabs[Math.max(0, index - 1)];
        }
    }
}
```

```css ./tabs-add-tab-vertical-example.css
:host {
    display: flex;
    width: 100%;
    max-width: 100%;
    height: 250px;
    --tab-nav-selected: var(--kbq-tabs-tab-item-filled-on-background-states-selected-background);
}

.example-tab-nav-bar {
    min-height: 0;
}

.example-tab-link {
    display: inline-block;
    text-align: left;
    overflow: hidden;
    text-overflow: ellipsis;
}

.example-tab-close {
    position: absolute;
    right: calc(var(--kbq-tabs-size-tab-item-padding-horizontal) - var(--kbq-tabs-size-tab-item-focus-outline-width));
    top: 0;
    bottom: 0;
    width: 32px;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    background: linear-gradient(var(--tab-nav-selected), var(--tab-nav-selected)), var(--kbq-background-bg);
    mask-image: linear-gradient(to right, transparent 0%, black 50%);
}

.example-tabs-add-tab-vertical__content {
    flex: 1;
    padding: var(--kbq-size-m);
    overflow: auto;
}

.example-tab-nav-bar__wrapper {
    display: flex;
    flex-direction: column;
    height: 100%;
    width: 240px;
    gap: var(--kbq-size-s);
}
```

### Hot keys

Horizontal and vertical tabs take different arrows, and the pair that does not match the orientation does nothing.

| Key           | Action                                          |
| ------------- | ----------------------------------------------- |
| ←             | Move the focus to the previous tab (horizontal) |
| →             | Move the focus to the next tab (horizontal)     |
| ↑             | Move the focus to the previous tab (vertical)   |
| ↓             | Move the focus to the next tab (vertical)       |
| Home          | Move the focus to the first tab                 |
| End           | Move the focus to the last tab                  |
| Space / Enter | Select the tab in focus                         |

### Accessibility

**Activation is manual.** Arrow keys move the focus without changing the selection: the tab in focus becomes the current one only on `Space` or `Enter`. This lets a user walk a long strip without loading every panel on the way.

**The strip is a single tab stop.** Exactly one tab carries `tabindex="0"` and the rest carry `-1`, so `Tab` enters and leaves the strip in one press and the arrows move within it. Disabled tabs stay in the strip but are skipped by the arrows, so a group whose first tab is disabled is still reachable.

**`KbqTabGroup` emits the full tabs pattern.** The label strip is a `tablist` (with `aria-orientation="vertical"` when `vertical` is set), each label is a `tab` with `aria-selected` and `aria-controls`, and each content region is a `tabpanel` with `aria-labelledby` pointing back at its label. The active panel takes a tab stop of its own so it is reachable even when it holds nothing focusable.

**`KbqTabNavBar` needs `[tabNavPanel]`.** Without it the nav bar is plain navigation: it emits no `role="tablist"`, its links emit no `role="tab"`/`aria-selected`, and the active link is marked with `aria-current="page"` instead. Bind `[tabNavPanel]` to a `kbqTabNavPanel` element whenever the nav bar switches a region on the same page rather than navigating away.

### State Saving

A tab group remembers which tab was selected and restores the state after a page reload. On by default — use `[useStateSaving]="false"` to turn it off on a specific component.

**Example: Tabs state saving** (`tabs-state-saving`)

```ts tabs-state-saving-example.ts
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqTabGroup, KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-state-saving-example',
    imports: [KbqTabsModule, KbqButtonModule],
    template: `
        <div class="layout-column layout-align-center-center docs-example-vertical-margin">
            <kbq-tab-group stateSavingKey="tabs-state-saving-example" style="width: 100%">
                <!-- A tabId is what makes the selection survive the tabs being reordered. -->
                <kbq-tab tabId="delivery">
                    <ng-template kbq-tab-label>Доставка</ng-template>
                    Данный текст используется для иллюстрации содержимого вкладки
                </kbq-tab>
                <kbq-tab tabId="payment">
                    <ng-template kbq-tab-label>Оплата</ng-template>
                    Данный текст используется для иллюстрации содержимого вкладки
                </kbq-tab>
                <kbq-tab tabId="refund">
                    <ng-template kbq-tab-label>Возврат</ng-template>
                    Данный текст используется для иллюстрации содержимого вкладки
                </kbq-tab>
            </kbq-tab-group>

            <button kbq-button class="layout-margin-top-l" type="button" (click)="group().clearSavedState()">
                Reset saved state
            </button>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsStateSavingExample {
    protected readonly group = viewChild.required(KbqTabGroup);
}
```

Nothing is persisted while `selectedIndex` or `activeTab` is bound: the selection belongs to whatever drives that binding.

Give the tabs a `tabId`. It is the only identity that survives the tabs being reordered — without it the selection falls back to a position, and a dev-mode warning says so.

`kbq-tab-nav-bar` never persists: the router decides which link is active, and the URL is the state worth restoring. `clearSavedState()` removes what is stored.

Keys, storage and expiry work the same for every component that persists — see [Saving component state](https://koobiq.io/en/components/core.md#saving-component-state).

### Recommendations

Use tabs in the following cases:

- For content navigation on a page when it's vital to have quick access to separate parts.
- For additional navigation.

Don't use tabs in the following cases:

- When you can place all content on a single page or screen. There's no need to hide some of its parts under tabs.
- For selecting values in forms. Don't confuse tabs with input boxes: [radio buttons](https://koobiq.io/en/components/radio.md) and [button toggles](https://koobiq.io/en/components/button-toggle.md).
- For main navigation. Use the [Navbar](https://koobiq.io/en/components/navbar.md) component instead.
- When you need to change the view for the same type of data (list, tile, or table). Instead, use a [button menu](https://koobiq.io/en/components/dropdown.md) or [button toggle](https://koobiq.io/en/components/button-toggle.md).
- When there's already content in a tab. You can't use both vertical and horizontal tabs at the same time. Instead, use fewer tabs, use only one type of tab, a button toggle, or button menu.
- If there's only one tab, you don't need a panel with tabs.
- For indicating steps in the form master. Tabs have a similar appearance to steps. To make sure that there is no confusion, a designer must use different styles for these elements. It's useful to leave a comment for a developer as well. For example: Note that these are form steps, not tabs.

## API

### Components

#### KbqTab (component)

```ts
@Component({ selector: 'kbq-tab', exportAs: 'kbqTab' })
class KbqTab {
    tooltipTitle = input<string | undefined>();
    disabled = input<boolean | undefined>();
    readonly tooltipPlacement = input<KbqPopUpPlacementValues>(
        PopUpPlacements.Right
    );
    readonly textLabel = input<string>('', { alias: 'label' });
    readonly empty = input<boolean>(false);
    readonly tabId = input<string>();
    templateLabel: KbqTabLabel;
    readonly explicitContent: Signal<TemplateRef<any> | undefined>;
    readonly implicitContent: Signal<TemplateRef<any>>;
    get iconOnlyLabel(): boolean;
    readonly stateChanges: Subject<void>;
    position: number | null;
    origin: number | null;
    isActive: boolean;
    get isOverflown(): boolean;
    overflowTooltipTitle: string;
}
```

- `[disabled]`: `boolean | undefined` — Whether the tab is disabled: it cannot be selected and has no interactive states.
- `[tooltipPlacement]`: `KbqPopUpPlacementValues` — Placement of the tooltip shown for a truncated or empty label.
- `[label]`: `string` — Plain text label for the tab, used when there is no template label.
- `templateLabel`: `KbqTabLabel` — The label template of the tab: the `kbqTabLabel` in its content, or else the one assigned in code.
- `explicitContent`: `Signal<TemplateRef<any> | undefined>` — Template provided in the tab content that will be used if present, used to enable lazy-loading
- `implicitContent`: `Signal<TemplateRef<any>>` — Template inside the KbqTab view that contains an `<ng-content>`.
- `iconOnlyLabel`: `boolean` — Whether the tab label contains only an icon (no text).
- `stateChanges`: `Subject<void>` — Emits whenever the internal state of the tab changes.
- `position`: `number | null` — The relatively indexed position where 0 represents the center, negative is left, and positive
  represents the right.
- `origin`: `number | null` — The initial relatively index origin of the tab if it was created and selected after there
  was already a selected tab. Provides context of what position the tab should originate from.
- `isActive`: `boolean` — Whether the tab is currently active. A template that reads it is updated when it changes.

#### KbqTabGroup (component)

Tab-group component.  Supports basic tab pairs (label + content) and includes keyboard navigation.

```ts
@Component({ selector: 'kbq-tab-group', exportAs: 'kbqTabGroup' })
class KbqTabGroup {
    readonly transparent = input<boolean>(false);
    readonly onSurface = input<boolean>(false);
    readonly underlined = input<boolean>(false);
    readonly vertical = input<boolean>(false);
    readonly dynamicHeight = input<boolean>(false);
    selectedIndex = model<number | undefined>();
    activeTab = model<KbqTabSelectBy | null | undefined>();
    headerPosition = input<KbqTabHeaderPosition | undefined>();
    animationDuration = input<string | undefined>();
    readonly focusChange = output<KbqTabChangeEvent>();
    readonly animationDone = output<void>();
    readonly selectedTabChange = output<KbqTabChangeEvent>();
    get tabs(): QueryList<KbqTab>;
    readonly tabBodyWrapper: Signal<ElementRef<any>>;
    readonly tabHeader: Signal<KbqTabHeader>;
    get hasSavedState(): boolean;
    focusChanged(index: number): void;
    getTabLabelId(i: number): string;
    getTabContentId(i: number): string;
    setTabBodyWrapperHeight(tabHeight: number): void;
    removeTabBodyWrapperHeight(): void;
    handleClick(
        tab: KbqTab,
        tabHeader: KbqTabHeader,
        index: number
    ): void;
    getTabIndex(
        tab: KbqTab,
        tabHeader: KbqTabHeader,
        index: number
    ): number;
    onSelectFocusedIndex($event: number): void;
    saveState(): void;
    clearSavedState(): void;
}
```

- `[transparent]`: `boolean` — Whether the tab strip background should be transparent.
- `[onSurface]`: `boolean` — Whether the group is rendered on a surface rather than on the page background.
- `[underlined]`: `boolean` — Whether the selected tab is marked with an underline instead of a filled background.
- `[vertical]`: `boolean` — Whether the tab strip is laid out vertically, alongside the content instead of above it.
- `[dynamicHeight]`: `boolean` — Whether the tab group should grow to the size of the active tab.
- `[(selectedIndex)]`: `number | undefined` — The index of the active tab.
- `[headerPosition]`: `KbqTabHeaderPosition | undefined` — Position of the tab header.
- `[animationDuration]`: `string | undefined` — Duration for the tab animation. Must be a valid CSS value (e.g. 600ms).
- `[useStateSaving]`: `boolean` — Whether the component remembers its state across reloads and restores it on the next render.
  Defaults to `true`, or to whatever `KBQ_STATE_SAVING_ENABLED` is provided as.
- `[stateSavingKey]`: `string` — The key the state is persisted under. While it is empty the key is derived from where the
  component sits in the document, which moves when the surrounding markup is restructured — an `id`
  on the component or any ancestor pins it just as well as this input does.
- `(focusChange)`: `KbqTabChangeEvent` — Event emitted when focus has changed within a tab group.
- `(animationDone)`: `void` — Event emitted when the body animation has completed
- `(selectedTabChange)`: `KbqTabChangeEvent` — Event emitted when the tab selection has changed.
- `tabs`: `QueryList<KbqTab>` — The tabs of the group.
- `hasSavedState`: `boolean` — Whether state is currently persisted for this tab group — restored on init, or written since.
  Always `false` while `useStateSaving` is unset, and `false` again after `clearSavedState()`.
- `getTabLabelId()`: `string` — Returns a unique id for each tab label element
- `getTabContentId()`: `string` — Returns a unique id for each tab content element
- `setTabBodyWrapperHeight()`: `void` — Sets the height of the body wrapper to the height of the activating tab if dynamic
  height property is true.
- `removeTabBodyWrapperHeight()`: `void` — Removes the height of the tab body wrapper.
- `handleClick()`: `void` — Handle click events, setting new selected index if appropriate.
- `getTabIndex()`: `number` — Retrieves the tabindex for the tab. The single `0` follows the header's focus position rather
  than the selection, so that a disabled selected tab cannot take the whole strip out of the tab
  order — a disabled tab is `-1`, never absent, so it stays a roving-tabindex participant.
- `saveState()`: `void` — Persists which tab is selected.

  Called for every selection a user makes. Both the tab's `tabId` and its position are stored: the
  `tabId` is the only one that survives the tabs being reordered, and the position is all there is
  when the tabs carry none.
- `clearSavedState()`: `void` — Removes the state persisted for this tab group.

  Persistence itself stays on — the next selection is written again. Unset `useStateSaving` to stop
  it.

#### KbqTabNavBar (component)

Navigation component matching the styles of the tab group header.

```ts
@Component({
    selector: '[kbqTabNavBar], [kbq-tab-nav-bar]',
    exportAs: 'kbqTabNavBar'
})
class KbqTabNavBar extends KbqPaginatedTabHeader {
    readonly transparent = input<boolean>(false);
    readonly onSurface = input<boolean>(false);
    readonly underlined = input<boolean>(false);
    readonly tabNavPanel = input<KbqTabNavPanel>();
    selectedIndex = input<number | undefined>();
    disablePagination = input<boolean | undefined>();
    vertical = input<boolean | undefined>();
    get tabListContainer(): ElementRef;
    get tabList(): ElementRef;
    get nextPaginator(): ElementRef<HTMLElement>;
    get previousPaginator(): ElementRef<HTMLElement>;
    get items(): QueryList<KbqTabLink>;
    get role(): string | null;
    focusIndex: number;
    readonly selectFocusedIndex: EventEmitter<number>;
    readonly indexFocused: EventEmitter<number>;
    showPaginationControls: boolean;
    disableScrollAfter: boolean;
    disableScrollBefore: boolean;
    readonly elementRef: ElementRef<HTMLElement>;
    updateActiveLink(): void;
    handleKeydown(event: KeyboardEvent): void;
    onContentChanges(): void;
    updatePagination(): void;
    isValidIndex(index: number): boolean;
    setTabFocus(tabIndex: number): void;
    getLayoutDirection(): Direction;
    scrollHeader(direction: ScrollDirection): void;
    handlePaginatorClick(direction: ScrollDirection): void;
    checkPaginationEnabled(): void;
    stopInterval(): void;
    handlePaginatorPress(
        direction: ScrollDirection,
        mouseEvent?: MouseEvent
    ): void;
}
```

- `[transparent]`: `boolean` — Whether the nav bar background should be transparent.
- `[underlined]`: `boolean` — Whether the nav bar should be underlined.
- `[tabNavPanel]`: `KbqTabNavPanel` — Associated tab panel controlled by the nav bar.
- `[selectedIndex]`: `number | undefined` — The index of the active tab.
- `[disablePagination]`: `boolean | undefined` — Whether pagination should be disabled. This can be used to avoid unnecessary
  layout recalculations if it's known that pagination won't be required.

  A vertical header never paginates, so `vertical` forces this on without overwriting the value
  the consumer set — turning `vertical` back off restores it.
- `[vertical]`: `boolean | undefined` — Whether the tabs should be displayed vertically.
- `tabListContainer`: `ElementRef` — The scroll container of the tab links.
- `tabList`: `ElementRef` — The element that holds the tab links.
- `nextPaginator`: `ElementRef<HTMLElement>` — The pagination arrow towards the end of the tab list.
- `previousPaginator`: `ElementRef<HTMLElement>` — The pagination arrow towards the beginning of the tab list.
- `items`: `QueryList<KbqTabLink>` — The tab links of the nav bar.
- `focusIndex`: `number` — Tracks which element has focus; used for keyboard navigation
- `selectFocusedIndex`: `EventEmitter<number>` — Event emitted when the option is selected.
- `indexFocused`: `EventEmitter<number>` — Event emitted when a label is focused.
- `showPaginationControls`: `boolean` — Whether the controls for pagination should be displayed
- `disableScrollAfter`: `boolean` — Whether the tab list can be scrolled more towards the end of the tab label list.
- `disableScrollBefore`: `boolean` — Whether the tab list can be scrolled more towards the beginning of the tab label list.
- `updateActiveLink()`: `void` — Notifies the component that the active link has been changed.
- `onContentChanges()`: `void` — Callback for when the MutationObserver detects that the content has changed.
- `updatePagination()`: `void` — Updates the view whether pagination should be enabled or not.

  WARNING: Calling this method can be very costly in terms of performance. It should be called
  as infrequently as possible from outside of the Tabs component as it causes a reflow of the
  page.
- `isValidIndex()`: `boolean` — Determines if an index is valid.  If the tabs are not ready yet, we assume that the user is
  providing a valid index and return true.
- `setTabFocus()`: `void` — Sets focus on the HTML element for the label wrapper and scrolls it into the view if
  scrolling is enabled.
- `getLayoutDirection()`: `Direction` — The layout direction of the containing app.
- `scrollHeader()`: `void` — Moves the tab list in the 'before' or 'after' direction (towards the beginning of the list or
  the end of the list, respectively).

  This is an expensive call that forces a layout reflow to compute box and scroll metrics and
  should be called sparingly.
- `handlePaginatorClick()`: `void` — Handles click events on the pagination arrows.
- `checkPaginationEnabled()`: `void` — Evaluate whether the pagination controls should be displayed. If the scroll width of the
  tab list is wider than the size of the header container, then the pagination controls should
  be shown.

  This is an expensive call that forces a layout reflow to compute box and scroll metrics and
  should be called sparingly.
- `stopInterval()`: `void` — Stops the currently-running paginator interval.
- `handlePaginatorPress()`: `void` — Handles the user pressing down on one of the paginators.
  Starts scrolling the header after a certain amount of time.
  - `direction`: `ScrollDirection` — In which direction the paginator should be scrolled.

### Directives

#### KbqAlignTabsCenterCssStyler (directive)

```ts
@Directive({
    selector: 'kbq-tab-group[kbq-align-tabs-center], [kbq-tab-nav-bar][kbq-align-tabs-center], [kbqTabNavBar][kbq-align-tabs-center]'
})
class KbqAlignTabsCenterCssStyler {}
```

#### KbqAlignTabsEndCssStyler (directive)

```ts
@Directive({
    selector: 'kbq-tab-group[kbq-align-tabs-end], [kbq-tab-nav-bar][kbq-align-tabs-end], [kbqTabNavBar][kbq-align-tabs-end]'
})
class KbqAlignTabsEndCssStyler {}
```

#### KbqStretchTabsCssStyler (directive)

```ts
@Directive({
    selector: 'kbq-tab-group[kbq-stretch-tabs], [kbq-tab-nav-bar][kbq-stretch-tabs], [kbqTabNavBar][kbq-stretch-tabs]'
})
class KbqStretchTabsCssStyler {}
```

#### KbqTabContent (directive)

Decorates the `ng-template` tags and reads out the template from it.

```ts
@Directive({ selector: '[kbqTabContent]' })
class KbqTabContent {
    template: TemplateRef<any>;
}
```

#### KbqTabLabel (directive)

Used to flag tab labels for use with the portal directive

```ts
@Directive({ selector: '[kbq-tab-label], [kbqTabLabel]' })
class KbqTabLabel extends CdkPortal {
    readonly iconOnly = input<boolean>(false);
    templateRef: TemplateRef<C>;
    viewContainerRef: ViewContainerRef;
    context?: C;
    injector?: Injector;
    get origin(): ElementRef<any>;
    get isAttached(): boolean;
    attach(host: PortalOutlet, context?: C): EmbeddedViewRef<C>;
    detach(): void;
    setAttachedHost(host: PortalOutlet | null): void;
}
```

- `[iconOnly]`: `boolean` — Whether the tab contains only an icon without a label.
- `templateRef`: `TemplateRef<C>` — The embedded template that will be used to instantiate an embedded View in the host.
- `viewContainerRef`: `ViewContainerRef` — Reference to the ViewContainer into which the template will be stamped out.
- `context?`: `C` — Contextual data to be passed in to the embedded view.
- `injector?`: `Injector` — The injector to use for the embedded view.
- `isAttached`: `boolean` — Whether this portal is attached to a host.
- `attach()`: `EmbeddedViewRef<C>` — Attach the portal to the provided `PortalOutlet`.
  When a context is provided it will override the `context` property of the `TemplatePortal`
  instance.
- `setAttachedHost()`: `void` — Sets the PortalOutlet reference without performing `attach()`. This is used directly by
  the PortalOutlet when it is performing an `attach()` or `detach()`.

#### KbqTabLink (directive)

Link inside of a KbqTabNavBar.

```ts
@Directive({ selector: '[kbqTabLink], [kbq-tab-link]', exportAs: 'kbqTabLink' })
class KbqTabLink {
    id = input<string | undefined>();
    active = input<boolean | undefined>();
    disabled = input<boolean | undefined>();
    get vertical(): boolean;
    get underlined(): boolean;
    readonly elementRef: ElementRef<HTMLElement>;
    focus(): void;
}
```

- `[id]`: `string | undefined` — Unique id for the link.
- `[active]`: `boolean | undefined` — Whether the link is active.
- `[disabled]`: `boolean | undefined` — Whether the tab link is disabled.
- `focus()`: `void` — Focuses the tab link.

  `preventScroll: true`: `FocusKeyManager.setActiveItem` calls this itself, right after (and
  unconditionally on) the `change` emission that runs `KbqPaginatedTabHeader.setTabFocus` — so
  without the guard here too, that call's own `preventScroll: true` is immediately undone by
  this one, and an overflowing nav bar double-jumps (native scroll-into-view, then the
  paginator-aware `scrollCorrection`) on every arrow-key press.

#### KbqTabNavPanel (directive)

Tab panel component associated with KbqTabNav.

```ts
@Directive({ selector: '[kbqTabNavPanel]', exportAs: 'kbqTabNavPanel' })
class KbqTabNavPanel {
    readonly id = input<string>(`kbq-tab-nav-panel-${nextUniqueId++}`);
    activeTabId?: string;
}
```

- `[id]`: `string` — Unique id for the tab panel.
- `activeTabId?`: `string` — Id of the active tab in the nav bar.

#### KbqVerticalTabsCssStyler (directive)

Deprecated: No-op. The vertical layout class is bound from `KbqTabGroup`/`KbqTabNavBar` themselves,
so it follows the `vertical` value instead of merely the presence of the attribute. Will be removed
in a future major version — stop importing it.

```ts
@Directive({
    selector: 'kbq-tab-group[vertical], [kbq-tab-nav-bar][vertical], [kbqTabNavBar][vertical]'
})
class KbqVerticalTabsCssStyler {}
```

### Classes

#### KbqTabChangeEvent (class)

A simple change event emitted on focus or selection changes.

```ts
class KbqTabChangeEvent {
    index: number;
    tab: KbqTab;
}
```

- `index`: `number` — Index of the currently-selected tab.
- `tab`: `KbqTab` — Reference to the currently-selected tab.

### Types

#### KbqPaginatedTabHeaderItem (type)

Item inside a paginated tab header.

```ts
type KbqPaginatedTabHeaderItem = FocusableOption & {
    elementRef: ElementRef;
};
```

#### KbqTabBodyPositionState (type)

These position states are used internally as the translation states of the tab body. Setting the
position state to left, right, or center will transition the tab body from its current
position to its respective state. If there is not current position (void, in the case of a new
tab body), then there will be no transition to its state.

In the case of a new tab body that should immediately be centered with an animating transition,
then left-origin-center or right-origin-center can be used, which will use left or right as its
pseudo-prior state.

```ts
type KbqTabBodyPositionState = 'left' | 'center' | 'right' | 'left-origin-center' | 'right-origin-center';
```

#### KbqTabHeaderPosition (type)

Possible positions for the tab header.

```ts
type KbqTabHeaderPosition = 'above' | 'below';
```

#### KbqTabsConfig (interface)

Object that can be used to configure the default options for the tabs module.

```ts
interface KbqTabsConfig {
    animationDuration?: string;
}
```

- `animationDuration?`: `string` — Duration for the tab animation. Must be a valid CSS value (e.g. 600ms).

#### KbqTabSelectBy (type)

```ts
type KbqTabSelectBy = string | number | ((tabs: KbqTab[]) => KbqTab | null);
```

#### KbqTabsState (interface)

The persisted state of a tab group — which tab was selected.

Both are stored: `tabId` is the only identifier that survives a reordering, and `index` is all there is
when the tabs carry no `tabId`.

```ts
interface KbqTabsState {
    tabId: string | null;
    index: number;
}
```

#### ScrollDirection (type)

The directions that scrolling can go in when the header's tabs exceed the header width. 'After'
will scroll the header towards the end of the tabs list and 'before' will scroll towards the
beginning of the list.

```ts
type ScrollDirection = 'after' | 'before';
```

### Constants

#### KBQ_TAB_LABEL (const)

```ts
const KBQ_TAB_LABEL: InjectionToken<KbqTabLabel>;
```

#### KBQ_TABS_CONFIG (const)

Injection token that can be used to provide the default options the tabs module.

```ts
const KBQ_TABS_CONFIG: InjectionToken<KbqTabsConfig>;
```

## Examples

### Select tab by ID

**Example: Tab list with selection by tab id** (`tabs-active-tab-id`)

```ts tabs-active-tab-id-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-active-tab-id-example',
    imports: [
        KbqTabsModule,
        KbqIconModule
    ],
    template: `
        <kbq-tab-group [(activeTab)]="selectedTabId">
            @for (tab of tabs; track tab) {
                <kbq-tab [tabId]="tab.tabId">
                    <ng-template kbq-tab-label>
                        <i [kbq-icon]="tab.icon"></i>
                        {{ tab.tabId }}
                    </ng-template>
                    Content for selected tab with id: {{ selectedTabId }}
                </kbq-tab>
            }
        </kbq-tab-group>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsActiveTabIdExample {
    readonly tabs = [
        { tabId: 'files', icon: 'kbq-folder-open_16' },
        { tabId: 'settings', icon: 'kbq-gear_16' },
        { tabId: 'tasks', icon: 'kbq-bars-horizontal_16' }
    ];

    selectedTabId: string = 'settings';
}
```

### Select tab by index

**Example: Tab list with selection by index** (`tabs-active-tab-index`)

```ts tabs-active-tab-index-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-active-tab-index-example',
    imports: [
        KbqTabsModule,
        KbqIconModule
    ],
    template: `
        <kbq-tab-group [(activeTab)]="selectedTabIndex">
            @for (tab of tabs; track tab) {
                <kbq-tab [tabId]="tab.tabId">
                    <ng-template kbq-tab-label>
                        <i [kbq-icon]="tab.icon"></i>
                        {{ tab.tabId }}
                    </ng-template>
                    Content for selected tab with index: {{ selectedTabIndex }}
                </kbq-tab>
            }
        </kbq-tab-group>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsActiveTabIndexExample {
    readonly tabs = [
        { tabId: 'files', icon: 'kbq-folder-open_16' },
        { tabId: 'settings', icon: 'kbq-gear_16' },
        { tabId: 'tasks', icon: 'kbq-bars-horizontal_16' }
    ];

    selectedTabIndex: number = 1;
}
```

### Using with other controls

Action buttons are conveniently placed in the same panel as underlined tabs.

**Example: Tabs actionbar** (`tabs-actionbar`)

```ts tabs-actionbar-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqTabsModule } from '@koobiq/components/tabs';

@Component({
    selector: 'tabs-actionbar-example',
    imports: [
        KbqTabsModule,
        KbqButtonModule,
        KbqIconModule
    ],
    template: `
        <div class="example-tabs-actionbar">
            <div class="example-tabs-actionbar_nav">
                <nav kbqTabNavBar underlined>
                    @for (dashboard of dashboards; track dashboard) {
                        <a kbqTabLink [active]="activeDashboard === dashboard" (click)="activeDashboard = dashboard">
                            {{ dashboard }}
                        </a>
                    }
                </nav>
            </div>

            <div class="example-tabs-actionbar_controls">
                <button class="kbq-button_transparent" color="contrast" kbq-button aria-label="List view">
                    <i kbq-icon="kbq-list_16"></i>
                </button>
                <button class="kbq-button_transparent" color="contrast" kbq-button aria-label="Filter">
                    <i kbq-icon="kbq-filter_16"></i>
                </button>
                <button color="contrast" kbq-button (click)="createDashboard()">
                    <i kbq-icon="kbq-plus_16"></i>
                    Create dashboard
                </button>
            </div>
        </div>
    `,
    styleUrls: ['tabs-actionbar-example.css'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabsActionbarExample {
    readonly dashboards = new Array(2).fill(0).map((_, index) => `Dashboard ${index}`);
    activeDashboard = this.dashboards[0];

    createDashboard(): void {
        this.dashboards.push(`Dashboard ${this.dashboards.length + 1}`);
    }
}
```

```css tabs-actionbar-example.css
.example-tabs-actionbar {
    display: flex;
}

.example-tabs-actionbar .example-tabs-actionbar_nav {
    overflow: hidden;
}

.example-tabs-actionbar .example-tabs-actionbar_controls {
    display: flex;
    flex-grow: 1;
    align-items: center;
    justify-content: end;
    position: relative;
}

.example-tabs-actionbar .example-tabs-actionbar_controls::before {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    right: 0;
    height: 1px;
    background: var(--kbq-line-contrast-less);
}

.example-tabs-actionbar .example-tabs-actionbar_controls button + button {
    margin-left: var(--kbq-size-s);
}
```
