# Select

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

## Overview

Select - allows the user to select one or more values from a predefined list.

**Example: Select** (`select-overview`)

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

@Component({
    selector: 'select-overview-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-select [value]="selected">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectOverviewExample {
    readonly options = Array.from({ length: 5 }).map((_, i) => `Option #${i}`);
    readonly selected = this.options[0];
}
```

### Validation

**Example: Select validation** (`select-validation`)

```ts select-validation-example.ts
import { afterNextRender, ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-validation-example',
    imports: [KbqSelectModule, ReactiveFormsModule],
    template: `
        <div class="kbq-form-vertical layout-column">
            <div class="kbq-form__label">Valid</div>
            <kbq-form-field>
                <kbq-select placeholder="Placeholder">
                    @for (option of options; track option) {
                        <kbq-option [value]="option">{{ option }}</kbq-option>
                    }
                </kbq-select>
            </kbq-form-field>
        </div>

        <div class="kbq-form-vertical layout-column">
            <div class="kbq-form__label">Invalid</div>
            <kbq-form-field>
                <kbq-select placeholder="Placeholder" [formControl]="invalidControl">
                    <kbq-option [value]="null">None</kbq-option>
                    @for (option of options; track option) {
                        <kbq-option [value]="option">{{ option }}</kbq-option>
                    }
                </kbq-select>
            </kbq-form-field>
        </div>
    `,
    styles: `
        :host {
            display: flex;
            gap: var(--kbq-size-l);
        }

        .kbq-form-vertical {
            width: 50%;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectValidationExample {
    readonly options = Array.from({ length: 10 }).map((_, i) => `Option #${i}`);
    readonly invalidControl = new FormControl(null, Validators.required);

    constructor() {
        afterNextRender(() => {
            this.invalidControl.markAsTouched();
            this.invalidControl.updateValueAndValidity();
        });
    }
}
```

### Inactive states

**Example: Select disabled** (`select-disabled`)

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

@Component({
    selector: 'select-disabled-example',
    imports: [KbqSelectModule],
    template: `
        <div class="example-row">
            <div class="kbq-form__label">For select</div>
            <kbq-form-field>
                <kbq-select placeholder="Placeholder" disabled [value]="selected">
                    @for (option of options; track option) {
                        <kbq-option [value]="option">{{ option }}</kbq-option>
                    }
                </kbq-select>
            </kbq-form-field>
        </div>

        <div class="example-row">
            <div class="kbq-form__label">For options</div>
            <kbq-form-field>
                <kbq-select placeholder="Placeholder" [value]="selected">
                    @for (option of options; track option) {
                        <kbq-option [disabled]="$odd" [value]="option">{{ option }}</kbq-option>
                    }
                </kbq-select>
            </kbq-form-field>
        </div>

        <div class="example-row">
            <div class="kbq-form__label">For optgroup</div>
            <kbq-form-field>
                <kbq-select placeholder="Placeholder" [value]="selected">
                    @for (group of groups; track group; let odd = $odd) {
                        <kbq-optgroup [label]="group" [disabled]="odd">
                            @for (option of options; track option) {
                                <kbq-option [value]="option">{{ option }}</kbq-option>
                            }
                        </kbq-optgroup>
                    }
                </kbq-select>
            </kbq-form-field>
        </div>
    `,
    styles: `
        .example-row {
            display: flex;
            align-items: center;
            justify-content: flex-end;
            width: 440px;
            margin: 0 auto;
            padding: var(--kbq-size-l);
            gap: var(--kbq-size-xxl);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectDisabledExample {
    readonly options = Array.from({ length: 5 }).map((_, i) => `Option #${i}`);
    readonly groups = Array.from({ length: 3 }).map((_, i) => `Group #${i}`);
    readonly selected = this.options[0];
}
```

### Reset selected value

The reset control clears everything except disabled options. Change that with the `clearPredicate` attribute: `[clearPredicate]="clearEverything"` and `clearEverything = () => true` clear the disabled ones too.

**Example: Select cleaner with disabled options** (`select-cleaner-with-disabled`)

```ts select-cleaner-with-disabled-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqOptionBase } from '@koobiq/components/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-cleaner-with-disabled-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-select multiple placeholder="Kept" [value]="selected">
                <kbq-cleaner />
                @for (option of options; track option.value) {
                    <kbq-option [value]="option.value" [disabled]="option.disabled">{{ option.value }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>

        <kbq-form-field>
            <kbq-select multiple placeholder="Cleared" [value]="selected" [clearPredicate]="clearEverything">
                <kbq-cleaner />
                @for (option of options; track option.value) {
                    <kbq-option [value]="option.value" [disabled]="option.disabled">{{ option.value }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            gap: var(--kbq-size-l);
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 240px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectCleanerWithDisabledExample {
    readonly options = Array.from({ length: 4 }).map((_, i) => ({
        value: `Option #${i}`,
        disabled: i === 1
    }));

    readonly selected = this.options.map(({ value }) => value);

    /** Opts out of the default, which leaves the disabled options selected. */
    readonly clearEverything = (_option: KbqOptionBase) => true;
}
```

### Multiple selection

Whichever order the values are picked in, the form control receives them in panel order — the order the options are declared in. Pass a `sortComparator` to order them differently.

**Example: Select multiple** (`select-multiple`)

```ts select-multiple-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-multiple-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-select multiple placeholder="Placeholder" [value]="selected">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }

                <kbq-cleaner />
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectMultipleExample {
    readonly options = Array.from({ length: 10 }).map((_, i) => `Option #${i}`);
    readonly selected = this.options.slice(0, 3);
}
```

### Multiple selection (multiline)

**Example: Select with multiline matcher** (`select-with-multiline-matcher`)

```ts select-with-multiline-matcher-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-with-multiline-matcher-example',
    imports: [
        KbqSelectModule
    ],
    template: `
        <div>
            <kbq-form-field>
                <kbq-select placeholder="Placeholder" [multiline]="true" [value]="selected" [panelWidth]="400">
                    @for (option of options; track option) {
                        <kbq-option [value]="option">{{ option }}</kbq-option>
                    }
                    <kbq-cleaner />
                </kbq-select>
            </kbq-form-field>
        </div>
    `,
    styles: [
        `
            :host {
                display: flex;
                flex-direction: column;
                align-items: center;
            }

            .kbq-form-field {
                width: 320px;
            }

            div:not(:last-child) {
                margin-bottom: var(--kbq-size-xl);
            }
        `
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectWithMultilineMatcherExample {
    readonly options = Array.from({ length: 10 }).map((_, i) => `Option ${i}`);
    selected: string[] = [this.options[0], this.options[1], this.options[2], this.options[3]];
}
```

A multiline trigger grows with every selected option, so a long selection can make it taller than the dropdown
itself. When that happens and the panel fits neither below the trigger nor above it, the panel is anchored to
the trigger's first row and drawn over the rest of them, opening one gap below that row. The first row, the
chevron and the cleaner stay visible and clickable.

### Customizing tags

In multiple selection mode the markup of every tag can be replaced with your own. Declare an `<ng-template #kbqSelectTagContent>` inside `<kbq-select>` — it is rendered once per selected option and receives the option as `$implicit` and the `KbqSelect` instance as `select`:

```html
<ng-template #kbqSelectTagContent let-option let-select="select">
    <kbq-tag [selectable]="false" [disabled]="option.disabled || select.disabled">
        {{ option.viewValue }}
        @if (!option.disabled && !select.disabled) {
            <i kbq-icon="kbq-xmark-s_16" kbqTagRemove (click)="select.onRemoveMatcherItem(option, $event)"></i>
        }
    </kbq-tag>
</ng-template>
```

The template replaces the built-in markup completely, so the color, the disabled state and the remove control have to be reproduced by hand. Keep `<kbq-tag>` as the root element: the “+N” counter of hidden tags measures the rendered `kbq-tag` elements, and any other root element breaks it.

### Grouping values

**Example: Select groups** (`select-groups`)

```ts select-groups-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-groups-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-select [value]="selected">
                @for (group of groups; track group) {
                    <kbq-optgroup [label]="group">
                        @for (option of options; track option) {
                            <kbq-option [value]="option">{{ option }}</kbq-option>
                        }
                    </kbq-optgroup>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectGroupsExample {
    readonly groups = Array.from({ length: 3 }).map((_, i) => `Group #${i}`);
    readonly options = Array.from({ length: 5 }).map((_, i) => `Option #${i}`);
    readonly selected = this.options[0];
}
```

### Filling a list of values

#### List with search

Use search when the list has more than 10 items. Search splits a multi-word query into parts and searches for them independently, trims leading and trailing spaces, is case-insensitive, and folds diacritics. The algorithm is described in the [Smart search guide](https://koobiq.io/en/other/search-smart.md).

**Example: Select search** (`select-search`)

```ts select-search-example.ts
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { createSearchPredicate, KbqHighlightModule, tokenizeSearchQuery } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqSelectModule, kbqSelectOptionsProvider } from '@koobiq/components/select';
import { map, startWith } from 'rxjs/operators';

@Component({
    selector: 'select-search-example',
    imports: [
        KbqSelectModule,
        KbqIconModule,
        KbqInputModule,
        AsyncPipe,
        ReactiveFormsModule,
        KbqHighlightModule
    ],
    template: `
        <kbq-form-field>
            <kbq-select multiline placeholder="Placeholder">
                <kbq-form-field noBorders kbqSelectSearch>
                    <i kbq-icon="kbq-magnifying-glass_16" kbqPrefix></i>
                    <input kbqInput type="text" [formControl]="searchControl" />
                    <kbq-cleaner />
                </kbq-form-field>

                <div kbq-select-search-empty-result>Nothing found</div>

                @for (option of filteredOptions | async; track option) {
                    <kbq-option [value]="option">
                        <span [innerHTML]="option | kbqHighlightBackground: searchTokens : true"></span>
                    </kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field-type-select {
            width: 320px;
        }
    `,
    providers: [
        kbqSelectOptionsProvider({ searchMinOptionsThreshold: 0 })
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectSearchExample {
    readonly options = [
        '10.125.123.0/24 - all',
        '10.125.10.0/24 - admin',
        '10.125.11.0/24 - guest',
        '172.16.0.0/16 - office',
        '192.168.1.0/24 - lab',
        '192.168.1.0/24 - Йота',
        'Café Wi-Fi guest network'
    ];

    readonly searchControl = new FormControl();
    readonly filteredOptions = this.searchControl.valueChanges.pipe(
        startWith(''),
        map((query) => this.search(query))
    );

    protected searchTokens: string[] = [];

    private search(query: string | null): string[] {
        this.searchTokens = tokenizeSearchQuery(query ?? '');

        return this.options.filter(createSearchPredicate(query ?? ''));
    }
}
```

#### Select all

In multiple selection mode all the values can be selected at once. The feature is off by default — turn it on with the `selectAll` attribute, and a master checkbox appears above the list.

```html
<kbq-select multiple selectAll placeholder="Placeholder">
    <kbq-option [value]="option">{{ option }}</kbq-option>
</kbq-select>
```

The checkbox has three states: unchecked when nothing is selected, indeterminate when only some options are, and checked when every option is. Clicking it while indeterminate selects the remaining options rather than clearing the selection. The label comes from the locale (`select.selectAll`).

Disabled options are ignored — they are neither selected nor deselected, and the checkbox state reflects only the options the user can actually toggle. When searching, the checkbox acts only on the results that match the query. Every action on the row emits a single `selectionChange` for the whole batch, followed by `onSelectAll`.

Not supported together with `withVirtualScroll` or `showPreselectedValues`: the row is not rendered when either is on, since "select all" can only act on the options currently rendered as `KbqOption`, not on the full virtualized or preselected data set.

**Example: Select all** (`select-select-all`)

```ts select-select-all-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-select-all-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-select multiple selectAll placeholder="Placeholder" [value]="selected">
                <kbq-cleaner />

                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field-type-select {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectSelectAllExample {
    readonly options = Array.from({ length: 6 }).map((_, i) => `Option ${i + 1}`);
    readonly selected = [this.options[1]];
}
```

Once everything is selected, the control can show a label of its own instead of literally listing the selected options. Project a `<kbq-select-trigger>` while `allOptionsSelected` is `true` — without it the select falls back to its default trigger. Use the element (or an element carrying the `kbq-select-trigger` attribute) rather than an `<ng-container>`: the trigger is stretched to the full width of the control by a rule on that element, and an `ng-container` leaves nothing for it to apply to, so the label and the arrow end up bunched together.

**Example: Select all label** (`select-select-all-label`)

```ts select-select-all-label-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-select-all-label-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-select #select="kbqSelect" multiple selectAll placeholder="Placeholder" [value]="selected">
                <!-- Projected only while everything is selected, so the rest of the time the select falls
                     back to its default trigger and lists the selected options as usual. -->
                @if (select.allOptionsSelected) {
                    <kbq-select-trigger>All options</kbq-select-trigger>
                }

                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field-type-select {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectSelectAllLabelExample {
    readonly options = Array.from({ length: 6 }).map((_, i) => `Option ${i + 1}`);
    readonly selected = [...this.options];
}
```

#### Selecting everything from the keyboard

`Ctrl`/`Cmd` + `A` selects all options in multiple selection mode. By default a repeated press keeps them selected; `selectAllToggle` makes it deselect them instead. With `selectAll` the shortcut always toggles both ways, so it and the master checkbox cannot disagree.

Inside a non-empty search field the first press selects the text of the field; the next one falls through to the options. The behaviour can be replaced wholesale with the `selectAllHandler` input — `onSelectAll` is then not emitted for the shortcut, as the handler owns the behaviour.

#### Search in Select for short lists

When there are only a few options, the search can be hidden automatically. This feature is off by default.
Use the `searchMinOptionsThreshold` attribute to configure it.

Possible values:

- `'auto'` – hides the search if there are fewer than 10 options (the default threshold).
- `<number>` – hides the search if there are fewer options than the specified number.

To configure this for all selects in a module with consistent display rules, use the `kbqSelectOptionsProvider` provider:

```ts
import { kbqSelectOptionsProvider } from '@koobiq/components/select';

@NgModule({
    providers: [
        kbqSelectOptionsProvider({ searchMinOptionsThreshold: 'auto' })
    ]
})
```

#### Footer

You can place auxiliary elements in the footer: actions, links, hints. It stays put while the list scrolls under it.

An action is drawn as a row of the drop-down menu rather than as a form button: add `kbq-select-footer-item` to a native `button` or `a`, and it lines up with the options above it. Any click inside the footer closes the panel.

With the panel open, `Tab` moves focus into the footer, visiting each interactive element in turn — an action row or a link alike — and `Shift + Tab` walks back the same way. Disabled controls are skipped, whether they carry the `disabled` attribute or the `kbq-disabled` class an `a` has to use instead. Once the controls run out, the next `Tab` closes the panel and returns focus to the field, as `Esc` does from anywhere in the footer.

**Example: Select footer** (`select-footer`)

```ts select-footer-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqLink } from '@koobiq/components/link';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-footer-example',
    imports: [
        KbqSelectModule,
        KbqIconModule,
        KbqLink
    ],
    template: `
        <div class="example-row">
            <div class="kbq-form__label">Button</div>
            <kbq-form-field class="example-select-field">
                <kbq-select [value]="selected">
                    @for (option of options; track option) {
                        <kbq-option [value]="option">{{ option }}</kbq-option>
                    }
                    <kbq-select-footer>
                        <button type="button" kbq-select-footer-item>
                            <i kbq-icon="kbq-plus_16"></i>
                            Button
                        </button>
                    </kbq-select-footer>
                </kbq-select>
            </kbq-form-field>
        </div>

        <div class="example-row">
            <div class="kbq-form__label">Caption</div>
            <kbq-form-field class="example-select-field">
                <kbq-select [value]="selected">
                    @for (option of options; track option) {
                        <kbq-option [value]="option">{{ option }}</kbq-option>
                    }
                    <kbq-select-footer class="example-select-footer-with-caption">Caption ⌥+⌘+F</kbq-select-footer>
                </kbq-select>
            </kbq-form-field>
        </div>

        <div class="example-row">
            <div class="kbq-form__label">Link</div>
            <kbq-form-field class="example-select-field">
                <kbq-select [value]="selected">
                    @for (option of options; track option) {
                        <kbq-option [value]="option">{{ option }}</kbq-option>
                    }
                    <kbq-select-footer>
                        <a href="https://koobiq.io/en/components/select" target="_blank" kbqLink external>Link</a>
                    </kbq-select-footer>
                </kbq-select>
            </kbq-form-field>
        </div>
    `,
    styles: `
        .example-row {
            display: flex;
            align-items: center;
            justify-content: flex-end;
            gap: var(--kbq-size-xxl);
            width: 400px;
            margin: 0 auto;
            padding: var(--kbq-size-l);
        }

        .example-select-field {
            width: 320px;
        }

        .example-select-footer-with-caption {
            color: var(--kbq-foreground-contrast-secondary);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectFooterExample {
    // Enough options to overflow the panel's default 256px — exactly eight rows fit — so the footer is
    // seen doing its job: staying put while the list scrolls under it.
    protected readonly options = Array.from({ length: 10 }).map((_, i) => `Option #${i}`);
    protected readonly selected = this.options[0];
}
```

### Select height

By default, the maximum height of the list is 256px. When there are many options, the drop-down menu becomes scrollable.

You can customize the height if needed. For example, in a normal menu, 7-8 items are visible. If there are 10 options to choose from, you can increase the height of the list and show all the items without hiding a small part under the scroll. Use the `panelMaxHeight` attribute with a value in pixels.

**Example: Select height** (`select-height`)

```ts select-height-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-height-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-label>panelMaxHeight</kbq-label>
            <kbq-select [panelMaxHeight]="500" [(value)]="value">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>

        <kbq-form-field>
            <kbq-label>--kbq-select-panel-size-max-height</kbq-label>
            <kbq-select [panelClass]="'example-select-panel-height'" [(value)]="themedValue">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        /* The same height set through the design token on a class passed via panelClass. */
        ::ng-deep .example-select-panel-height.kbq-select__panel {
            --kbq-select-panel-size-max-height: 500px;
        }

        :host {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: var(--kbq-size-l);
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectHeightExample {
    readonly options = Array.from({ length: 10 }).map((_, i) => `Option #${i + 1}`);
    protected readonly value = model(this.options[0]);
    protected readonly themedValue = model(this.options[0]);
}
```

`panelMaxHeight` caps the scrollable option list. A search field and a footer are rendered beside the list, so they add to the total height of the panel. A value larger than the space left in the viewport is clipped by the overlay rather than scrolled. With `cdk-virtual-scroll-viewport` the value is an exact height rather than a cap, because a virtual scroller needs a definite height.

To set the height for all selects within a module that share common display rules, you can use the `kbqSelectOptionsProvider`.

```ts
import { kbqSelectOptionsProvider } from '@koobiq/components/select';

@NgModule({
    providers: [
        kbqSelectOptionsProvider({ panelMaxHeight: 400 })
    ]
})
```

For theming, the same height is available as the `--kbq-select-panel-size-max-height` token — set it on `:root` to change every panel at once, or on a class passed through `panelClass` to change a single one. The example above shows both approaches side by side.

### Select width

#### Basic settings

The width of the select matches the select by default and expands if the list contains long text.

**Example: Select with panelWidth default** (`select-with-panel-width-default`)

```ts select-with-panel-width-default-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-with-panel-width-default-example',
    imports: [
        KbqSelectModule
    ],
    template: `
        <kbq-form-field>
            <kbq-select placeholder="Placeholder" [(value)]="valueFirst">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>

        <kbq-form-field>
            <kbq-select placeholder="Placeholder" [(value)]="valueSecond">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        .kbq-form-field:first-of-type {
            min-width: 68%;
            max-width: 68%;
        }

        .kbq-form-field:last-of-type {
            min-width: 32%;
            max-width: 32%;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-l layout-align-center-center layout-row layout-gap-l'
    }
})
export class SelectWithPanelWidthDefaultExample {
    readonly options = [
        'Use of Alternate Authentication Data',
        'Use of Command-Line Interpreters and Scripts',
        'Use of XSL Scripts',
        'System Owner or User Discovery',
        'Permission Group Discovery'
    ];

    protected readonly valueFirst = model(this.options[0]);
    protected readonly valueSecond = model(this.options[1]);
}
```

The select panel has a default `min-width` of 200px to align nicely with narrow selects. You can customize this by setting the `panelMinWidth` attribute with a numeric value.

**Example: Select with panelWidth min-width** (`select-with-panel-min-width`)

```ts select-with-panel-min-width-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-with-panel-min-width-example',
    imports: [
        KbqSelectModule
    ],
    template: `
        <kbq-form-field>
            <kbq-select [(value)]="value">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        .kbq-form-field {
            min-width: 104px;
            max-width: 104px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-l layout-align-center-center layout-row'
    }
})
export class SelectWithPanelMinWidthExample {
    readonly options = [
        'HUMINT',
        'HostIntel',
        'Intel',
        'OSINT',
        'Recon',
        'Scanning',
        'UserIntel'
    ];

    protected readonly value = model(this.options[0]);
}
```

To set the minimum width of select panel for all selects within a module that share common display rules, you can use the `kbqSelectOptionsProvider`.

```ts
import { kbqSelectOptionsProvider } from '@koobiq/components/select';

@NgModule({
    providers: [
        kbqSelectOptionsProvider({ panelMinWidth: 350 })
    ]
})
```

#### Additional options

If needed, the select width can be set to match the select width exactly. To do this, use the `panelWidth` attribute with the value `auto`. The panel still never gets narrower than `panelMinWidth` — set `panelMinWidth` to `0` if you want the panel to follow a narrow select exactly.

**Example: Select with panelWidth auto** (`select-with-panel-width-auto`)

```ts select-with-panel-width-auto-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-with-panel-width-auto-example',
    imports: [
        KbqSelectModule
    ],
    template: `
        <kbq-form-field>
            <kbq-select placeholder="Placeholder" [panelWidth]="'auto'" [(value)]="value">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        .kbq-form-field {
            width: 280px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-l layout-align-center-center layout-row'
    }
})
export class SelectWithPanelWidthAutoExample {
    readonly options = [
        'Active Scanning',
        'Business Intelligence Gathering',
        'Closed-Source Intelligence',
        'Open-Source Intelligence',
        'Network Infrastructure Recon',
        'User Reconnaissance',
        'Host Reconnaissance'
    ];

    protected readonly value = model(this.options[0]);
}
```

To set a fixed width of 400px for the select, use the `panelWidth` attribute with the value `400`. A fixed width is used exactly as given, so `panelMinWidth` is not applied to it.

**Example: Select with panelWidth fixed** (`select-with-panel-width-fixed`)

```ts select-with-panel-width-fixed-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-with-panel-width-fixed-example',
    imports: [
        KbqSelectModule
    ],
    template: `
        <kbq-form-field>
            <kbq-select placeholder="Placeholder" [panelWidth]="400" [(value)]="value">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-l layout-align-center-center layout-row'
    }
})
export class SelectWithPanelWidthFixedExample {
    readonly options = [
        'Active Scanning',
        'Business Intelligence Gathering',
        'Closed-Source Intelligence',
        'Open-Source Intelligence',
        'Network Infrastructure Recon',
        'User Reconnaissance',
        'Host Reconnaissance'
    ];

    protected readonly value = model(this.options[1]);
}
```

A panel growing with long option text stops at 640px. The cap is soft — it never makes the panel narrower than the select, and never clamps an explicit `panelWidth`. Raise or lower it for one select with `panelMaxWidth`, or for the whole application by setting the `--kbq-panel-size-width-max` token on `:root` (it is shared with [Tree-select](https://koobiq.io/en/components/tree-select.md), [Autocomplete](https://koobiq.io/en/components/autocomplete.md) and [Dropdown](https://koobiq.io/en/components/dropdown.md)).

To configure the select width for all selects within a module according to unified display rules, use the `kbqSelectOptionsProvider`.

```ts
import { kbqSelectOptionsProvider } from '@koobiq/components/select';

@NgModule({
    providers: [
        kbqSelectOptionsProvider({ panelWidth: 'auto' })
    ]
})
```

### Customized selection

The select can contain preselected values.

**Example: Selected preselected values** (`select-preselected-values`)

```ts select-preselected-values-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-preselected-values-example',
    imports: [KbqSelectModule, ReactiveFormsModule],
    template: `
        <kbq-form-field>
            <kbq-select placeholder="Placeholder" multiple [showPreselectedValues]="true" [formControl]="control">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }

                <kbq-cleaner />
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectPreselectedValuesExample {
    options = Array.from({ length: 15 }).map((_, i) => `Option #${i}`);
    control = new FormControl(['Option #101', 'Option #102']);
}
```

To make navigation easier, selected items can be pinned to the top of the list.

**Example: Selected prioritized selected** (`select-prioritized-selected`)

```ts select-prioritized-selected-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-prioritized-selected-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-select placeholder="Placeholder" multiple [(value)]="selected" (openedChange)="openedChange($event)">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }

                <kbq-cleaner />
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectPrioritizedSelectedExample {
    options = Array.from({ length: 15 }).map((_, i) => `Option #${i}`);
    selected = [this.options.at(-1)!, this.options.at(-2)!];

    constructor() {
        this.groupSelectedOptions();
    }

    openedChange(isOpened: boolean): void {
        if (!isOpened) this.groupSelectedOptions();
    }

    private groupSelectedOptions(): void {
        const unselected = this.options.filter((option) => !this.selected.includes(option));

        this.options = this.selected.concat(unselected);
    }
}
```

### Optional icon

**Example: Select icon** (`select-icon`)

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

@Component({
    selector: 'select-icon-example',
    imports: [KbqSelectModule, KbqIconModule],
    template: `
        <kbq-form-field>
            <i kbqPrefix color="contrast-fade" kbq-icon="kbq-globe_16"></i>
            <kbq-select [(value)]="selected">
                <kbq-select-matcher class="kbq-select__matcher">
                    <div class="kbq-select__match-container">
                        <span class="kbq-select__matcher-text">
                            {{ selected }}
                        </span>
                    </div>
                    <div class="kbq-select__arrow-wrapper">
                        <i class="kbq-select__arrow" color="contrast-fade" kbq-icon="kbq-chevron-down-s_16"></i>
                    </div>
                </kbq-select-matcher>
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectIconExample {
    readonly options = Array.from({ length: 5 }).map((_, i) => `Option #${i}`);
    selected = this.options[0];
}
```

### Using cdk-virtual-scroll-viewport

Add `cdk-virtual-scroll-viewport` to your component template to display only visible elements and improve performance.

When option values are objects, provide a `virtualOptionFactory` that maps a value to a `KbqVirtualOption` carrying the display label (and optionally a per-value `disabled` state). The factory is used whenever the selected value's `KbqOption` is not currently rendered — virtual scroll recycled it out of the viewport, the value was set programmatically before its option was rendered, or the value is missing from the currently loaded data (server-side search). The same `KbqVirtualOption` powers the trigger label in single mode and tag labels in multiple mode.

Narrowing the data source down — a search field over the same array — keeps the selected value in the trigger even while its option is filtered out of the list.

Collapsing the panel around an empty result uses the CSS `:has()` selector, supported by every browser the library targets (Chrome 105+, Edge 105+, Safari 15.4+, Firefox 121+). An older browser keeps the full viewport height instead of shrinking to the message; nothing else changes.

**Example: Select virtual scroll** (`select-virtual-scroll`)

```ts select-virtual-scroll-example.ts
import { CdkVirtualScrollViewport, ScrollingModule } from '@angular/cdk/scrolling';
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { KbqVirtualOption } from '@koobiq/components/core';
import { KbqScrollbarViewport } from '@koobiq/components/scrollbar';
import { KbqSelectModule } from '@koobiq/components/select';

type OptionItem = { id: number; label: string };

@Component({
    selector: 'select-virtual-scroll-example',
    imports: [KbqSelectModule, ScrollingModule, KbqScrollbarViewport],
    template: `
        <kbq-form-field>
            <kbq-select
                [compareWith]="compareWith"
                [virtualOptionFactory]="virtualOptionFactory"
                [(value)]="selected"
                (openedChange)="openedChange($event)"
            >
                <cdk-virtual-scroll-viewport kbqScrollbarViewport itemSize="32" minBufferPx="300" maxBufferPx="300">
                    <kbq-option *cdkVirtualFor="let option of options" [value]="option">
                        {{ option.label }}
                    </kbq-option>
                </cdk-virtual-scroll-viewport>
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectVirtualScrollExample {
    readonly options: OptionItem[] = Array.from({ length: 10000 }).map((_, i) => ({ id: i, label: `Option #${i}` }));
    readonly virtualScrollViewport = viewChild.required(CdkVirtualScrollViewport);

    selected: OptionItem = this.options[0];

    readonly compareWith = (a: OptionItem | null, b: OptionItem | null) => a?.id === b?.id;
    readonly virtualOptionFactory = (value: OptionItem) => new KbqVirtualOption(value, false, value.label);

    openedChange(isOpened: boolean): void {
        if (!isOpened) return;
        this.virtualScrollViewport().scrollToIndex(this.selected.id);
    }
}
```

### Layering

By default, the dropdown menu appears above the horizontal [Navbar](https://koobiq.io/en/components/navbar.md) and [Topbar](https://koobiq.io/en/components/top-bar.md). To have it slide under them while the page scrolls, as in the example below, mark the content area with `kbqOverlayLayer` — see [Panels under a sticky header](https://koobiq.io/en/components/core.md#panels-under-a-sticky-header).

**Example: select-scrolling-and-layering** (`select-scrolling-and-layering`)

```ts select-scrolling-and-layering-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
    selector: 'select-scrolling-and-layering-example',
    template: `
        <iframe
            src="/examples/select-scrolling-and-layering-page"
            loading="lazy"
            width="648"
            height="400"
            title="select-scrolling-and-layering-example"
            style="border: none"
        ></iframe>
    `,
    styles: `
        :host {
            display: flex;
            margin: -20px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectScrollingAndLayeringExample {}
```

### Caption in options

**Example: select two line option** (`select-two-line-option`)

```ts select-two-line-option-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-two-line-option-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-select multiple [value]="selected">
                @for (option of options; track option) {
                    <kbq-option [value]="option" [viewValue]="option">
                        <div>{{ option }}</div>
                        <div class="kbq-option-caption">caption</div>
                    </kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            align-items: center;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectTwoLineOptionExample {
    readonly options = Array.from({ length: 5 }).map((_, i) => `Option #${i}`);
    readonly selected = [this.options[0]];
}
```

### Localization

The strings the select renders itself — the `select all` label, the hidden-items counter, the accessible names of the cleaner and of the tag remove buttons — follow the active locale. Bind `[localeOverrides]` to override them for one instance; every key you leave out keeps following the locale. See the [localization guide](https://koobiq.io/en/main/localization.md) for the other ways to override them.

**Example: Select locale configuration** (`select-locale-configuration`)

```ts select-locale-configuration-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-locale-configuration-example',
    imports: [KbqFormFieldModule, KbqSelectModule],
    template: `
        <kbq-form-field>
            <kbq-label>Default strings</kbq-label>
            <kbq-select multiple selectAll placeholder="Placeholder" [value]="selected">
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>

        <kbq-form-field>
            <kbq-label>Overridden for this instance only</kbq-label>
            <!-- Keyed by locale section, so one binding also reaches the accessible name of the cleaner
                 projected into the select. Every string it does not mention keeps following the locale. -->
            <kbq-select
                multiple
                selectAll
                placeholder="Placeholder"
                [value]="selected"
                [localeOverrides]="{
                    select: { selectAll: 'Take everything' },
                    a11y: { clear: 'Drop the selection' }
                }"
            >
                <kbq-cleaner />
                @for (option of options; track option) {
                    <kbq-option [value]="option">{{ option }}</kbq-option>
                }
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            gap: var(--kbq-size-l);
            padding: var(--kbq-size-l);
        }

        .kbq-form-field-type-select {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectLocaleConfigurationExample {
    readonly options = Array.from({ length: 4 }).map((_, i) => `Option ${i + 1}`);
    readonly selected = [this.options[0]];
}
```

### Accessibility

The select is announced as a `combobox` that owns a `listbox`. `aria-expanded` follows the panel and `aria-controls` points at the option list while it is open. Opening the panel moves DOM focus into it: onto the option the arrow keys are on, which is how that option announces itself, or onto the search field when one is projected.

The select is a custom element, so a `<label for>` cannot name it. Inside a `kbq-form-field` the label does it through `aria-labelledby`, which the select wires up on its own:

```html
<kbq-form-field>
    <kbq-label>Country</kbq-label>
    <kbq-select [formControl]="control">…</kbq-select>
</kbq-form-field>
```

Without a form-field label, name the select with `aria-label` — it names both the trigger and the option list:

```html
<kbq-select aria-label="Country">…</kbq-select>
```

With neither, the `placeholder` becomes the name — a placeholder is rendered as ordinary text, which names nothing on its own.

Every option is an `option` carrying `aria-selected` and `aria-disabled`; `kbq-optgroup` is a `group` named by its own heading. The "select all" row stays an option too and reports the batch state on `aria-checked`, including `mixed` for a partial selection. The pseudo-checkbox next to an option is decorative and hidden from assistive technology — the selected state is already on the option itself.

While the panel shows something other than options — the loading, error or empty state, or an empty search result — the option list is marked `aria-busy`, so it is not announced as a list of options with nothing in it.

In multiple selection mode every tag has its own remove control: a `button` operable with `Enter` and `Space`. Its name is built from the a11y locale (`remove`) and the value's own text, so it is announced as "Remove Country", not just "Remove". Only the tags the trigger actually shows take a place in the tab order — the ones the "+N" counter stands for are clipped out of sight, and a tab stop there would move focus to something invisible. A custom `#kbqSelectTagContent` template replaces that markup wholesale — reproduce the control if you keep tag removal.

When the operating system asks for reduced motion, the panel opens and closes without the fade animation.

### Recommendations

- If you are using a select without a label, we suggest adding a placeholder to indicate what information the user should select. For example, “Country.”
- If there are more than 10 items in the list of values, then enable the search display in the select header. This will make it easier to find values.

## API

### Components

#### KbqSelect (component)

```ts
@Component({ selector: 'kbq-select', exportAs: 'kbqSelect' })
class KbqSelect extends KbqAbstractSelect implements ControlValueAccessor, KbqFormFieldControl<any>, CanUpdateErrorState, KbqSiblingPopup, KbqOptionParentComponent {
    readonly hiddenItemsText = input<string>();
    readonly hiddenItemsTextFormatter = input<(hiddenItemsText: string, hiddenItems: number) => string>(
        (hiddenItemsText, hiddenItems) => hiddenItemsText.replace('{{ number }}', hiddenItems.toString())
    );
    readonly ariaLabel = input<string | null>(null, {
        alias: 'aria-label'
    });
    readonly showPreselectedValues = input<boolean>(false);
    readonly triggerValuesLimit = input<number>(0);
    readonly panelClass = input<string | string[] | Set<string> | Record<string, boolean>>();
    readonly backdropClass = input<string>(
        'cdk-overlay-transparent-backdrop'
    );
    readonly errorStateMatcher = input<ErrorStateMatcher>();
    readonly sortComparator = input<(a: KbqOptionBase, b: KbqOptionBase, options: KbqOptionBase[]) => number>();
    readonly multiline = input<boolean>(false);
    readonly searchMinOptionsThreshold = input<'auto' | number | undefined>();
    readonly scrolledToBottomOffset = input<number>(0);
    readonly hasBackdrop = input<boolean>(false);
    readonly placeholder = input<string>();
    readonly required = input<boolean>(false);
    multiple = input<boolean>(false);
    readonly compareWith = input<(o1: any, o2: any) => boolean>(
        (o1, o2) => o1 === o2
    );
    readonly virtualOptionFactory = input<(value: any) => KbqVirtualOption>();
    readonly clearPredicate = input<(option: KbqOptionBase) => boolean>(
        this.defaultOptions?.clearPredicate ?? ((option) => !option.disabled)
    );
    readonly selectAllToggle = input<boolean>(false);
    readonly selectAll = input<boolean>(false);
    selectAllHandler = input<((event: KeyboardEvent, select: KbqSelect) => void) | undefined>();
    readonly panelWidth = input<KbqPanelWidth>(
        this.defaultOptions?.panelWidth ?? null
    );
    readonly panelMinWidth = input<KbqPanelMinWidth>(
        this.defaultOptions?.panelMinWidth === undefined
            ? KBQ_PANEL_DEFAULT_MIN_WIDTH
            : this.defaultOptions.panelMinWidth
    );
    readonly panelMaxWidth = input<KbqPanelMaxWidth>(
        this.defaultOptions?.panelMaxWidth === undefined ? null : this.defaultOptions.panelMaxWidth
    );
    readonly panelMaxHeight = input<KbqPanelMaxHeight>(
        this.defaultOptions?.panelMaxHeight === undefined ? null : this.defaultOptions.panelMaxHeight
    );
    readonly value = model<any>();
    readonly id = input<string | undefined>();
    readonly disabled = input<boolean>(false);
    readonly beforeOpened = output<void>();
    readonly selectionChange = output<KbqSelectChange>();
    readonly onSelectAll = output<KbqSelectAllEvent<KbqOption, KbqSelect>>();
    readonly scrolledToBottom = output<void>();
    defaultErrorStateMatcher: ErrorStateMatcher;
    elementRef: ElementRef<HTMLElement>;
    parentForm: NgForm | null;
    parentFormGroup: FormGroupDirective | null;
    ngControl: NgControl | null;
    readonly errorState: Signal<boolean>;
    controlType: string;
    readonly isNativeLabelSupported: false;
    readonly hiddenItems: Signal<number>;
    triggerFontSize: number;
    selectionModel: SelectionModel<KbqOptionBase>;
    previousSelectionModelSelected: KbqOptionBase[];
    keyManager: ActiveDescendantKeyManager<KbqOption>;
    transformOrigin: string;
    panelDoneAnimatingStream: Subject<string>;
    scrollStrategy: ScrollStrategy;
    positions: ConnectedPosition[];
    readonly trigger: Signal<ElementRef<any> | undefined>;
    readonly panel: Signal<ElementRef<any> | undefined>;
    readonly optionsContainer: Signal<ElementRef<any>>;
    readonly selectAllOption: Signal<KbqOption | undefined>;
    readonly footer: Signal<ElementRef<any> | undefined>;
    readonly cdkVirtualForOf: Signal<CdkVirtualForOf<any> | undefined>;
    readonly virtualScrollViewport: Signal<CdkVirtualScrollViewport | undefined>;
    get tags(): QueryList<KbqTag>;
    readonly customTrigger: Signal<KbqSelectTrigger | undefined>;
    readonly customMatcher: Signal<KbqSelectMatcher | undefined>;
    readonly customTagTemplateRef: Signal<TemplateRef<any> | undefined>;
    get options(): QueryList<KbqOption>;
    readonly optionGroups: Signal<readonly KbqOptgroup[]>;
    readonly search: Signal<KbqSelectSearch | undefined>;
    readonly searchEmpty: Signal<KbqSelectSearchEmptyResult | undefined>;
    readonly optionSelectionChanges: Observable<KbqOptionSelectionChange>;
    readonly openedChange: EventEmitter<boolean>;
    readonly openedStream: Observable<void>;
    readonly closedStream: Observable<void>;
    readonly focused: Signal<boolean>;
    panelOpen: boolean;
    get isAttached(): boolean;
    withVirtualScroll: boolean;
    get isEmptySearchResult(): boolean;
    get allOptionsSelected(): boolean;
    get selectAllState(): KbqPseudoCheckboxState;
    get selected(): KbqOptionBase | KbqOptionBase[];
    get triggerValue(): string;
    get triggerValues(): KbqOptionBase[];
    readonly empty: Signal<boolean>;
    get noOptions(): boolean;
    get firstSelected(): KbqOptionBase | null;
    get firstFiltered(): boolean;
    get multiSelection(): boolean;
    calculateHiddenItems: () => void;
    updateErrorState(): void;
    resetSearch(): void;
    toggle(): void;
    open(): void;
    openPanel(): void;
    close(): void;
    isRtl(): boolean;
    handleKeydown(event: KeyboardEvent): void;
    onFocus(): void;
    onBlur(): void;
    onAttached(): void;
    getPanelTheme(): string;
    focus(): void;
    onRemoveMatcherItem(option: KbqOptionBase, $event: Event): void;
    getItemHeight(): number;
    handleClick($event: MouseEvent): void;
}
```

- `[hiddenItemsText]`: `string` — Template string for the hidden items counter. Supports the `{{ number }}` placeholder.

  Takes precedence over the value the locale service provides, and keeps it for the whole lifetime of
  the select — switching the locale no longer overwrites what the consumer asked for.
- `[hiddenItemsTextFormatter]`: `(hiddenItemsText: string, hiddenItems: number) => string` — Formats the hidden items counter.
- `[aria-label]`: `string | null` — Accessible name of the trigger and of the option list.

  Only needed for a select rendered without a `kbq-form-field` label — with one, the label names both.
- `[showPreselectedValues]`: `boolean` — Determines whether preselected values are displayed.
- `[triggerValuesLimit]`: `number` — Specifies the maximum number of trigger values allowed.
  This constant limits the size of the trigger values array to ensure performance
  and prevent excessive memory usage.
  A value of `0` indicates that there is no limit.
- `[panelClass]`: `string | string[] | Set<string> | Record<string, boolean>` — Classes to be passed to the select panel. Supports the same syntax as the `[class]` binding.
- `[backdropClass]`: `string` — Classes to be passed to the overlay backdrop.
- `[errorStateMatcher]`: `ErrorStateMatcher` — Object used to control when error messages are shown.
- `[sortComparator]`: `(a: KbqOptionBase, b: KbqOptionBase, options: KbqOptionBase[]) => number` — Function used to sort the values in a select in multiple mode.
  Follows the same logic as `Array.prototype.sort`.

  Without one the values are emitted in panel order — the order the options are declared in, not the
  order they were picked in. A value whose option is not rendered (a `KbqVirtualOption` under virtual
  scroll or `showPreselectedValues`) has no place in that order and sorts after everything that has.
- `[multiline]`: `boolean` — Whether to use a multiline matcher or not. Default is false.
  When true, allows multiple lines of text in the selected value display.
- `[searchMinOptionsThreshold]`: `'auto' | number | undefined` — Controls when the search functionality is displayed based on the number of available options.

  Automatically enables search hiding if value provided, even if `defaultOptions.searchMinOptionsThreshold` is provided.

  Accepts `'auto'`, which resolves to `KBQ_SELECT_SEARCH_MIN_OPTIONS_THRESHOLD`; the getter therefore
  reports the resolved number, never `'auto'`. Defaults to `undefined`.
- `[scrolledToBottomOffset]`: `number` — Distance in pixels from the bottom of the options panel at which `scrolledToBottom` fires.
  Default `0` — emits only when the panel is fully scrolled to the bottom.
- `[hasBackdrop]`: `boolean` — Whether the overlay should have a backdrop.
  When true, clicking the backdrop will close the select.
- `[placeholder]`: `string` — Placeholder text to be shown when no value is selected.
  Displayed in the trigger when the select is closed and no value is selected.
- `[required]`: `boolean` — Whether the select is required. Affects validation and display of placeholder.
- `[multiple]`: `boolean` — Whether multiple options can be selected.
  Note: This cannot be changed dynamically after initialization.
- `[compareWith]`: `(o1: any, o2: any) => boolean` — Function to compare the option values with the selected values.
  The first argument is a value from an option.
  The second is a value from the selection.
  Should return true if the values match.
  Defaults to strict equality comparison.
- `[virtualOptionFactory]`: `(value: any) => KbqVirtualOption` — Factory used to construct a `KbqVirtualOption` for selected values whose
  `KbqOption` is not currently rendered (e.g. virtual scroll out of viewport,
  programmatically set initial value, `showPreselectedValues`).

  Required when option values are objects so the trigger (and tags in
  multiple mode) can render a human-readable label. Also lets consumers
  customise per-value `disabled` state or any future `KbqVirtualOption`
  fields without adding new `@Input`s.

  Defaults to `new KbqVirtualOption(value, this.disabled())`, which is correct
  for primitive values where `value` itself is the display label.
- `[clearPredicate]`: `(option: KbqOptionBase) => boolean` — Decides which selected options the projected `KbqCleaner` removes: return `true` to clear the
  option, `false` to keep it. Disabled options are kept by default. Bind a stable reference — a new
  function on every change detection re-runs the predicate over the whole selection.

  Not consulted by `writeValue` / `reset()`, which always clear everything.
- `[selectAllToggle]`: `boolean` — When `true`, a repeated Ctrl/Cmd+A deselects all options. Off by default (Ctrl+A only selects).
- `[selectAll]`: `boolean` — Whether to render the "select all" master checkbox above the options. Multiple selection only.

  The row acts on the options the user can actually toggle — rendered, enabled and selectable. Since
  search filtering removes options from the DOM, "select all" under an active query selects only the
  matches. Enabling it also makes Ctrl/Cmd + A a two-way toggle, so the shortcut and the checkbox
  never disagree (`selectAllToggle` is implied).
- `[selectAllHandler]`: `((event: KeyboardEvent, select: KbqSelect) => void) | undefined` — Function for handling the Ctrl + A (select all) keyboard combination.
  By default, the internal handler selects all options.
  It is called with the keyboard event that triggered it and a reference to this select component.
- `[panelWidth]`: `KbqPanelWidth` — Width of the panel. If set to `auto`, the panel will match the trigger width, but will never be
  narrower than `panelMinWidth`. If set to null or an empty string, the panel will grow to match the
  longest option's text. Any other value is used as an exact width, and `panelMinWidth` is not applied.
- `[panelMinWidth]`: `KbqPanelMinWidth` — Minimum width of the panel in pixels.
  If minWidth is larger than window width, it will be ignored.
- `[panelMaxWidth]`: `KbqPanelMaxWidth` — Maximum width of the panel in pixels. Caps how far the panel grows with its content — it never makes
  the panel narrower than the trigger, and never clamps an explicit `panelWidth`.
  When null, the `--kbq-panel-size-width-max` token applies.
- `[panelMaxHeight]`: `KbqPanelMaxHeight` — Maximum height of the panel's scrollable option list, in pixels. Applied as the
  `--kbq-select-panel-size-max-height` custom property on the panel, so it also drives the pinned height
  of a `cdk-virtual-scroll-viewport` — with virtual scroll the value is an exact height, not a cap.

  The search field and the footer sit outside the scrollable area and add to the panel's total height.
  When null, the token default (256px) applies.
- `[(value)]`: `any` — Value of the select control. Can be a single value or array of values for multiple selection. Follows
  `[value]`, the form control and the user's selection. A value set in code selects its options on the next
  change detection, without emitting `valueChange`.
- `[id]`: `string | undefined` — Unique identifier for the select component.
  Auto-generates an ID if not provided.
- `[disabled]`: `boolean` — Whether the select is disabled.
  When disabled, the select cannot be opened and its value cannot be changed. Also set by the bound form
  control.
- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.
- `(beforeOpened)`: `void` — Event emitted before the select panel starts opening.
- `(selectionChange)`: `KbqSelectChange` — Event emitted when the selected value has been changed by the user.
- `(onSelectAll)`: `KbqSelectAllEvent<KbqOption, KbqSelect>` — Event emitted when all options are selected or deselected via the Ctrl/Cmd + A shortcut.
  Not emitted when a custom `selectAllHandler` is supplied — the handler owns the behaviour then.
- `(scrolledToBottom)`: `void` — Emits when the options panel is scrolled to (or within `scrolledToBottomOffset` of) the bottom.
- `errorState`: `Signal<boolean>` — Whether the component is in an error state.
- `controlType`: `string` — A name for this control that can be used by `kbq-form-field`.
- `isNativeLabelSupported`: `false` — A `kbq-select` is not a native labelable element, so the form-field's `<label for>` cannot
  associate with it. The select is named through `aria-labelledby` instead.
- `hiddenItems`: `Signal<number>` — Number of the selected items that do not fit into the trigger.
- `triggerFontSize`: `number` — The cached font-size of the trigger element in pixels.
- `selectionModel`: `SelectionModel<KbqOptionBase>` — Deals with the selection logic. Manages selected options and their states.
- `previousSelectionModelSelected`: `KbqOptionBase[]` — Previously selected options before the current selection was made.
- `keyManager`: `ActiveDescendantKeyManager<KbqOption>` — Manages keyboard events for options in the panel.
- `transformOrigin`: `string` — The value of the select panel's transform-origin property.
- `panelDoneAnimatingStream`: `Subject<string>` — Emits once the panel has rendered (`'showing'`) or has been removed (`'void'`).
- `scrollStrategy`: `ScrollStrategy` — Strategy that will be used to handle scrolling while the select panel is open.
- `positions`: `ConnectedPosition[]` — Opens the panel below the trigger, falling back to above it when it does not fit.

  A third position is appended at runtime by `updatePanelAnchor`: a multiline trigger that has grown
  taller than the panel gets it anchored to its first row and drawn over the rest of it.
- `trigger`: `Signal<ElementRef<any> | undefined>` — Trigger - is a clickable field to open select dropdown panel
- `panel`: `Signal<ElementRef<any> | undefined>` — Reference to the overlay panel element.
- `optionsContainer`: `Signal<ElementRef<any>>` — Reference to the container element that holds the options.
- `selectAllOption`: `Signal<KbqOption | undefined>` — Reference to the built-in "select all" row, rendered only while `selectAll` is on.
- `footer`: `Signal<ElementRef<any> | undefined>` — Reference to the optional footer element in the panel.
- `cdkVirtualForOf`: `Signal<CdkVirtualForOf<any> | undefined>` — Reference to the CDK virtual scroll directive for virtual scrolling support.
- `virtualScrollViewport`: `Signal<CdkVirtualScrollViewport | undefined>` — Reference to the CDK virtual scroll viewport for tracking scroll position in virtual mode.
- `tags`: `QueryList<KbqTag>` — Query list of tags displayed in multiple selection mode.
- `customTrigger`: `Signal<KbqSelectTrigger | undefined>` — User-supplied override of the trigger element for custom rendering.
- `customMatcher`: `Signal<KbqSelectMatcher | undefined>` — User-supplied matcher component for custom value matching logic.
- `customTagTemplateRef`: `Signal<TemplateRef<any> | undefined>` — Custom template reference for rendering tag content.
- `options`: `QueryList<KbqOption>` — All of the defined select options.
- `optionGroups`: `Signal<readonly KbqOptgroup[]>` — All of the defined groups of options.
- `search`: `Signal<KbqSelectSearch | undefined>` — Reference to the optional search component.
- `searchEmpty`: `Signal<KbqSelectSearchEmptyResult | undefined>` — Reference to the optional empty search result component.
- `optionSelectionChanges`: `Observable<KbqOptionSelectionChange>` — Combined stream of all of the child options' change events.
- `openedChange`: `EventEmitter<boolean>` — Event emitted when the select panel has been toggled. Emits true when opened, false when closed.
  Also serves as the `openedChange` member of the `KbqSiblingPopup` contract — a tooltip sharing
  this element's host reacts to it, so its emission timing (gated on `panelDoneAnimatingStream`, see
  `ngOnInit`) matters beyond this output's original consumers.
- `openedStream`: `Observable<void>` — Event emitted when the select has been opened.
- `closedStream`: `Observable<void>` — Event emitted when the select has been closed.
- `focused`: `Signal<boolean>` — Whether the select is focused.
- `panelOpen`: `boolean` — Whether the select panel is currently open.
- `isAttached`: `boolean` — Whether the overlay panel is currently on screen. Part of the `KbqSiblingPopup` contract.
- `withVirtualScroll`: `boolean` — Whether virtual scrolling is enabled for the options panel.
- `isEmptySearchResult`: `boolean` — Whether the search returned no results.
- `allOptionsSelected`: `boolean` — Whether every option "select all" can act on is selected.
- `selectAllState`: `KbqPseudoCheckboxState` — State of the "select all" master checkbox.
- `selected`: `KbqOptionBase | KbqOptionBase[]` — Returns the currently selected option(s). Single value or array for multiple selection.
- `triggerValue`: `string` — Returns the display value for the trigger element.
- `triggerValues`: `KbqOptionBase[]` — Returns all selected options in display order.
- `empty`: `Signal<boolean>` — Whether no option is currently selected.
- `noOptions`: `boolean` — Whether there are no options available.
- `firstSelected`: `KbqOptionBase | null` — Returns the first selected option that is not disabled.
- `firstFiltered`: `boolean` — Whether the first selected option is filtered (not visible in the list).
- `multiSelection`: `boolean` — Whether multiple choice is enabled. True if multiple or multiline mode is active.
- `calculateHiddenItems`: `() => void` — Calculates the number of hidden items in multiple selection mode and updates `hiddenItems`.
- `updateErrorState()`: `void` — Updates the error state based on the error state matcher.
- `resetSearch()`: `void` — Resets the search component if present.
- `toggle()`: `void` — Toggles the overlay panel open or closed.
- `open()`: `void` — Triggers the opening of the panel component.
  If the component is disabled or the panel is already open, the call is ignored.
  Otherwise, the (beforeOpened) event is emitted.
  When no options are available, the panel is opened after a short delay;
  if options exist, it opens immediately.
- `openPanel()`: `void` — Internal method to open the panel. Handles overlay positioning and sizing.
  Sets up the overlay dimensions based on trigger size and configured options.
- `close()`: `void` — Closes the overlay panel.
- `isRtl()`: `boolean` — Checks if the current direction is RTL (right-to-left).
  - returns `boolean` — True if RTL mode is active, false otherwise.
- `handleKeydown()`: `void` — Handles all keyboard events for the select.
  Delegates to appropriate handler based on panel state.
  - `event`: `KeyboardEvent` — The keyboard event to handle.
- `onFocus()`: `void` — Handles focus event on the select element.
- `onBlur()`: `void` — Calls the touched callback only if the panel is closed. Otherwise, the trigger will
  "blur" to the panel when it opens, causing a false positive.
- `onAttached()`: `void` — Callback that is invoked when the overlay panel has been attached.
  Sets up position change subscription and closing actions.
- `getPanelTheme()`: `string` — Returns the theme to be used on the panel based on parent form field color.
- `focus()`: `void` — Focuses the select element.
- `onRemoveMatcherItem()`: `void` — Handles removal of a matched item in the trigger.
  - `option`: `KbqOptionBase` — The option to remove from selection.
  - `$event`: `Event` — The mouse or keyboard event that triggered the removal.
- `getItemHeight()`: `number` — Gets the height of a single option item.
  - returns `number` — The height in pixels of the first option, or 0 if no options exist.
- `handleClick()`: `void` — Handles click events on the select.
  Closes the panel if click is inside the footer.
  - `$event`: `MouseEvent` — The mouse event to handle.

#### KbqSelectError (component)

This component renders the error for a Select component.
The error message can be displayed in two visual variants: a default centered layout (when [paging] is falsy)
or a variant with left‑aligned text suitable for pagination controls (when [paging] is truthy).

```ts
@Component({
    selector: 'kbq-select-error, [kbq-select-error]',
    exportAs: 'kbqSelectError'
})
class KbqSelectError {
    readonly paging = input<boolean>(false);
}
```

- `[paging]`: `boolean` — Indicates whether styles for pagination controls should be used.

#### KbqSelectLoading (component)

```ts
@Component({
    selector: 'kbq-select-loading, [kbq-select-loading]',
    exportAs: 'kbqSelectLoading'
})
class KbqSelectLoading {}
```

#### KbqSelectNoOptions (component)

Component that represents the empty state of a select dropdown.

```ts
@Component({
    selector: 'kbq-select-no-options, [kbq-select-no-options]',
    exportAs: 'kbqSelectNoOptions'
})
class KbqSelectNoOptions {}
```

### Directives

#### KbqOptionTooltip (directive)

```ts
@Directive({ selector: 'kbq-option' })
class KbqOptionTooltip extends KbqTooltipTrigger {
    readonly ignoreTooltipPointerEvents = input<boolean>(true);
    readonly forDisabledComponent = input<Record<'disabledSignal', WritableSignal<boolean>>>();
    readonly singleInstance = input<boolean>(
        inject(KBQ_TOOLTIP_SINGLE_INSTANCE_DEFAULT),
        { alias: 'kbqTooltipSingleInstance' }
    );
    hideWithTimeout = input<boolean | undefined>();
    get textElement(): HTMLElement;
    get isOverflown(): boolean;
    tooltipVisible: boolean;
    tooltipPlacement: KbqPopUpPlacementValues;
    relativeToPointer: boolean;
    relativeToCaret: boolean;
    relativeToCaretVertical: KbqCaretVerticalAnchor;
    tooltipPlacementPriority: string | string[] | null;
    content: string | TemplateRef<unknown>;
    disabled: boolean;
    enterDelay: number;
    leaveDelay: number;
    trigger: string;
    customClass: string;
    context: unknown;
    color: KbqComponentColors | string;
    arrow: boolean;
    offset: number | null;
    readonly placementChange: EventEmitter<KbqPopUpPlacementValues>;
    readonly visibleChange: EventEmitter<boolean>;
    modifier: KbqEnumValues<TooltipModifier>;
    header: string | TemplateRef<unknown>;
    isOpen: boolean;
    get isAttached(): boolean;
    get openedChange(): Observable<boolean>;
    handleElementEnter(): void;
    handleElementLeave(): void;
    show(delay?: number): void;
    hide(delay?: number): void;
    showForMouseEvent(event: MouseEvent): void;
    showForElement(element: HTMLElement): void;
    updateOrigin(origin: KbqOverlayOrigin | null): void;
}
```

- `[ignoreTooltipPointerEvents]`: `boolean` — Overrides the tooltip's hoverable default. The overflow hint floats over the neighbouring options of a
  scrolling list, so a pointer-capturing pane would swallow the click that selects them.
- `[forDisabledComponent]`: `Record<'disabledSignal', WritableSignal<boolean>>` — Input for controlling the disabled state of a component.

  The input expects a component containing `disabledSignal` property, which is
  a writable signal emitting boolean values.
- `[kbqTooltipSingleInstance]`: `boolean` — Whether the tooltip takes part in the "only one tooltip is visible at a time" group: showing it
  hides the previously visible tooltip, and it is hidden when another tooltip is shown.

  Defaults to `true`; the default can be changed application-wide with the
  `KBQ_TOOLTIP_SINGLE_INSTANCE_DEFAULT` token.

  Tooltips without an interactive `kbqTrigger` (`manual`, `none`, …) never take part in the group,
  regardless of this input — they are shown imperatively and are expected to stay pinned.
- `[hideWithTimeout]`: `boolean | undefined` — Changes hiding behavior. By default, tooltip is hidden on mouseleave from trigger.
  Setting hideWithTimeout to true will delay tooltip hiding and will not hide when the mouse moves from trigger
  to tooltip.
- `tooltipVisible`: `boolean` — Input (`kbqVisible`) that programmatically shows or hides the tooltip; reflects the current `visible` state.
- `tooltipPlacement`: `KbqPopUpPlacementValues` — Input (`kbqPlacement`) that sets the tooltip placement relative to its trigger; reflects the current `placement`.
- `relativeToPointer`: `boolean` — Positions the tooltip relative to the mouse cursor. Only available for top and bottom kbqPlacement.
  Does not work with kbqPlacementPriority.
- `relativeToCaret`: `boolean` — Positions the tooltip relative to the text caret of the field it is attached to, following it while the
  user types, moves the caret and scrolls the field. Anchors to the selection whenever there is one.

  Every placement is available, unlike with `kbqRelativeToPointer`: the caret is anchored as a rectangle,
  so the usual fallback placements keep the tooltip on screen. Wins when both inputs are enabled.

  The field is the host element itself when it is editable, otherwise the first editable it wraps.
- `relativeToCaretVertical`: `KbqCaretVerticalAnchor` — What the tooltip is vertically anchored to while `kbqRelativeToCaret` is enabled.
- `tooltipPlacementPriority`: `string | string[] | null` — Input (`kbqPlacementPriority`) that sets the ordered fallback placements; reflects the current `placementPriority`.
- `content`: `string | TemplateRef<unknown>` — Input (`kbqTooltip`) with the tooltip content — a string or a template. Updating it refreshes an open tooltip.
- `disabled`: `boolean` — Input (`kbqTooltipDisabled`) controlling whether the tooltip is disabled; setting it to `true` hides it.
  `undefined` leaves the decision to the trigger, like an unset input.

  Reads back the *effective* state, which for triggers that derive one of their own (see
  `derivedDisabledWins`) is not necessarily the value that was written.
- `enterDelay`: `number` — Input (`kbqEnterDelay`) — delay in milliseconds before the tooltip is shown. Defaults to `400`.
- `leaveDelay`: `number` — Input (`kbqLeaveDelay`) — delay in milliseconds before the tooltip is hidden. Defaults to `0`.
- `trigger`: `string` — Input (`kbqTrigger`) with the comma-separated trigger events. An empty value resets to hover + focus and rebinds listeners.
- `customClass`: `string` — Input (`kbqTooltipClass`) with an extra CSS class applied to the tooltip; updating it refreshes the class map.
- `context`: `unknown` — Input (`kbqTooltipContext`) with the context object passed to a template tooltip; updating it refreshes the open tooltip.
- `color`: `KbqComponentColors | string` — Input (`kbqTooltipColor`) with the tooltip color theme. Defaults to `KbqComponentColors.Contrast`.
- `arrow`: `boolean` — Input (`kbqTooltipArrow`) — whether to render the tooltip's arrow/pointer. Defaults to `false`.
- `offset`: `number | null` — Input (`kbqTooltipOffset`) — distance in pixels between the tooltip and its trigger. `null`, the default, keeps
  the gap of the stylesheet.
- `placementChange`: `EventEmitter<KbqPopUpPlacementValues>` — Output (`kbqPlacementChange`) that emits the new placement whenever the tooltip repositions.
- `visibleChange`: `EventEmitter<boolean>` — Output (`kbqVisibleChange`) that emits when the tooltip's visibility changes.
- `modifier`: `KbqEnumValues<TooltipModifier>` — Visual variant of the tooltip. Accepts `'default'` | `'warning'` | `'extended'`.
  Replaces the removed `KbqWarningTooltipTrigger` and `KbqExtendedTooltipTrigger`
  subclasses — to render a warning tooltip use `kbqTooltipModifier="warning"`,
  and `kbqTooltipModifier="extended"` for the extended variant (combine with `kbqTooltipHeader`).
- `header`: `string | TemplateRef<unknown>` — Header text or template, rendered above the tooltip content. Only meaningful with
  `kbqTooltipModifier="extended"`. Replaces the removed `KbqExtendedTooltipTrigger.header`.
- `isOpen`: `boolean` — Whether the pop-up overlay is currently open.
- `isAttached`: `boolean` — Whether the pop-up overlay is currently attached.

  Unlike `isOpen`, which follows the visibility of the pop-up component and therefore lags a task behind
  `show()`/`hide()`, this flips synchronously — it is the window during which the overlay (and its
  backdrop) is present in the DOM. Part of the `KbqSiblingPopup` contract.
- `openedChange`: `Observable<boolean>` — Emits `true` when the pop-up opens and `false` when it closes. Part of the `KbqSiblingPopup` contract.
- `show()`: `void` — Shows the tooltip after `delay` ms. Suppresses showing while a pop-up on the same element holds the
  anchor or on a focus trigger that did not originate from the keyboard, and applies cursor-relative
  positioning when `relativeToPointer` is enabled.
- `hide()`: `void` — Hides the tooltip after `delay` ms and, when the user has genuinely left the host, un-mutes a tooltip
  that a pop-up on the same element had muted.

  `mouseleave` and `blur` are the release signals because the base class binds `hide` to both regardless
  of whether the tooltip is currently visible. A pop-up that is still attached does not count: its
  backdrop steals the hover from the trigger while the pointer stands still, and that `mouseleave` must
  not be mistaken for the user leaving.
- `showForMouseEvent()`: `void` — Shows the tooltip next to the cursor position of the given mouse event, anchoring it to the event's
  `currentTarget` instead of the directive's own host element.

  Use it when the trigger cannot be declared in the template — for instance for a tooltip shared by the
  cells of a virtualized table. The directive still has to be created in an injection context.
- `showForElement()`: `void` — Shows the tooltip anchored to the given element instead of the directive's own host element.

  Use it when the trigger cannot be declared in the template. The directive still has to be created in an
  injection context:

  ```ts
  const tooltip = runInInjectionContext(injector, () => new KbqTooltipTrigger());
  tooltip.showForElement(element);
  ```

  Does nothing while the tooltip is disabled or has no content — there is no overlay to anchor then.
- `updateOrigin()`: `void` — Positions the pop-up against `origin` — an element, or a rectangle in viewport coordinates such as the one
  `kbqCreateCaretOrigin` keeps on the caret — instead of the host element; `null` restores the host
  element. An open pop-up moves to the new origin at once.

  This moves the position origin only. `setExternalNativeElement` moves the host itself, and with it
  the trigger's event listeners, its focus target and the scroll containers the pop-up follows.

#### KbqSelectErrorText (directive)

Marks an element as the error text container for a *KbqSelect* component.

```ts
@Directive({
    selector: '[kbq-select-error-text]',
    exportAs: 'kbqSelectErrorText'
})
class KbqSelectErrorText {}
```

### Classes

#### KbqSelectChange (class)

Change event object that is emitted when the select value has changed.

```ts
class KbqSelectChange {
    source: KbqSelect;
    value: any;
    constructor(source: KbqSelect, value: any);
}
```

### Types

#### KbqSelectMatcherMeasurement (type)

Widths the matcher needs in order to work out how many of the selected tags actually fit.

```ts
type KbqSelectMatcherMeasurement = {
    totalItemsWidth: number;
    totalVisibleItemsWidth: number;
    visibleItems: number;
};
```

- `totalItemsWidth`: `number` — Total width of the selected tags, laid out without the "+N" counter.
- `totalVisibleItemsWidth`: `number` — Total width of the tags that stay on the first line while the counter is shown.
- `visibleItems`: `number` — How many tags stay on the first line while the counter is shown.

#### KbqSelectOptions (type)

Options for the `kbq-select` that can be configured using the `KBQ_SELECT_OPTIONS` injection token.

```ts
type KbqSelectOptions = Partial<{
    panelWidth: KbqPanelWidth;
    panelMinWidth: KbqPanelMinWidth;
    panelMaxWidth: KbqPanelMaxWidth;
    panelMaxHeight: KbqPanelMaxHeight;
    searchMinOptionsThreshold: 'auto' | number;
    clearPredicate: (option: KbqOptionBase) => boolean;
}>;
```

- `panelWidth?`: `KbqPanelWidth` — Width of the panel. If set to `auto`, the panel will match the trigger width.
  If set to null or an empty string, the panel will grow to match the longest option's text.
- `panelMinWidth?`: `KbqPanelMinWidth` — Minimum width of the panel. If minWidth is larger than window width or property set to null, it will be ignored.
- `panelMaxWidth?`: `KbqPanelMaxWidth` — Maximum width of the panel. Caps growth by content only — it never overrides the trigger width or an
  explicit `panelWidth`. If null, the `--kbq-panel-size-width-max` token applies.
- `panelMaxHeight?`: `KbqPanelMaxHeight` — Maximum height of the panel's scrollable option list. Does not include the search field or the
  footer. If null, the `--kbq-select-panel-size-max-height` token applies.
- `searchMinOptionsThreshold?`: `'auto' | number` — Whether to enable hiding search by default if options is less than minimum.

  - `'auto'` uses `KBQ_SELECT_SEARCH_MIN_OPTIONS_THRESHOLD` as min value.
  - number - will enables search hiding and uses value as min.
- `clearPredicate?`: `(option: KbqOptionBase) => boolean` — Decides which selected options the projected `KbqCleaner` removes. Disabled options are kept when
  this is not set. Overridden per instance by the `clearPredicate` attribute.

### Constants

#### delayBeforeDisplayingResultWithoutOptions (const)

Delay in milliseconds before displaying a result when there are no options.

```ts
const delayBeforeDisplayingResultWithoutOptions: number;
```

#### KBQ_SELECT_HIDDEN_TEXT_VISIBLE_CLASS (const)

Class that reveals the "+N" counter the matcher reserves room for.

```ts
const KBQ_SELECT_HIDDEN_TEXT_VISIBLE_CLASS: string;
```

#### KBQ_SELECT_OPTIONS (const)

Injection token that can be used to provide the default options for the `kbq-select`.

```ts
const KBQ_SELECT_OPTIONS: InjectionToken<KbqSelectOptions>;
```

#### kbqSelectOptionsProvider (const)

Utility provider for `KBQ_SELECT_OPTIONS`.

```ts
const kbqSelectOptionsProvider: (options: KbqSelectOptions) => Provider;
```

#### minimumTimeToDisplayLoading (const)

Minimum time in milliseconds to display the loading state.

```ts
const minimumTimeToDisplayLoading: number;
```

## Examples

### Loading Options

If the options have not loaded yet, open the panel with a 100 ms delay. In most cases, the list will finish loading within that brief moment, and the user will not perceive it as interface lag. If the data still has not loaded, show the menu with a loading indicator for at least 300 ms to avoid unpleasant flickering.

**Example: Select loading** (`select-loading`)

```ts select-loading-example.ts
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqButtonToggleModule } from '@koobiq/components/button-toggle';
import { KbqHighlightBackgroundPipe, KbqVirtualOption } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import {
    delayBeforeDisplayingResultWithoutOptions,
    KbqSelectModule,
    minimumTimeToDisplayLoading
} from '@koobiq/components/select';
import { BehaviorSubject, catchError, Observable, of, race, switchMap, throwError, timer } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

type Option = {
    id: number;
    label: string;
};

export class OptionsApiService {
    private delay: number = 100;

    private data = Array.from({ length: 10 }).map((_, i) => ({ id: i, label: `Option #${i}` }));

    setDelay(delay: number) {
        this.delay = delay;
    }

    getOptions(search: string): Observable<Option[]> {
        if (search === 'error') {
            return throwError(() => new Error('error'));
        }

        const term = search.toLowerCase().trim();

        const filtered = !term ? this.data : this.data.filter((option) => option.label.toLowerCase().includes(term));

        return timer(this.delay).pipe(map(() => filtered));
    }
}

export class SelectFacade {
    private readonly api = inject(OptionsApiService);
    private readonly search$ = new BehaviorSubject<string | null>(null);

    reset(): void {
        this.search$.next(null);
    }

    setSearch(value: string) {
        this.search$.next(value);
    }

    setDelay(delay: number) {
        this.api.setDelay(delay);
    }

    readonly state$ = this.search$.pipe(
        // for use in a real project
        // debounceTime(300),
        // distinctUntilChanged(),
        switchMap((search) => {
            if (search === null) {
                return of({ status: 'idle' } as const);
            }

            const request$ = this.api.getOptions(search).pipe(
                map((data) =>
                    data.length === 0 ? ({ status: 'empty' } as const) : ({ status: 'success', data } as const)
                ),
                catchError((error: any) => of({ status: 'error', error } as const))
            );

            return race(
                request$,
                timer(delayBeforeDisplayingResultWithoutOptions).pipe(
                    switchMap(() => {
                        return request$.pipe(
                            startWith({ status: 'loading' } as const),
                            switchMap((state) => {
                                if (state.status === 'loading') {
                                    return of(state);
                                }

                                return timer(minimumTimeToDisplayLoading).pipe(map(() => state));
                            })
                        );
                    })
                )
            );
        }),
        startWith({ status: 'error', error: { message: 'error message' } } as const)
    );
}

@Component({
    selector: 'select-loading-example',
    imports: [
        KbqSelectModule,
        KbqButtonToggleModule,
        FormsModule,
        KbqIconModule,
        KbqInputModule,
        ReactiveFormsModule,
        AsyncPipe,
        KbqHighlightBackgroundPipe,
        KbqButtonModule
    ],
    template: `
        <div class="layout-column layout-align-center-start layout-gap-l">
            <div class="layout-column layout-gap-xs">
                Option Loading Time
                <kbq-button-toggle-group [ngModel]="100" (ngModelChange)="facade.setDelay($event)">
                    <kbq-button-toggle [value]="100">100 ms</kbq-button-toggle>
                    <kbq-button-toggle [value]="250">250 ms</kbq-button-toggle>
                    <kbq-button-toggle [value]="500">500 ms</kbq-button-toggle>
                    <kbq-button-toggle [value]="3000">3000 ms</kbq-button-toggle>
                </kbq-button-toggle-group>
            </div>

            <kbq-form-field>
                <kbq-select
                    [compareWith]="compareWith"
                    [showPreselectedValues]="true"
                    [value]="selectedOption"
                    [virtualOptionFactory]="virtualOptionFactory"
                    (beforeOpened)="loadOptions()"
                    (closed)="resetOptions()"
                >
                    <kbq-form-field noBorders kbqSelectSearch>
                        <i kbq-icon="kbq-magnifying-glass_16" kbqPrefix></i>
                        <input kbqInput type="text" autocomplete="off" [formControl]="searchControl" />
                        <kbq-cleaner />
                    </kbq-form-field>

                    @if (state$ | async; as state) {
                        @switch (state.status) {
                            @case ('loading') {
                                <kbq-select-loading />
                            }
                            @case ('empty') {
                                <div kbq-select-search-empty-result>Ничего не найдено</div>
                            }
                            @case ('error') {
                                <!--{{ state.error.message }}-->
                                <kbq-select-error>
                                    <span kbq-select-error-text>Не удалось показать записи</span>
                                    <button
                                        kbq-button
                                        [kbqStyle]="'transparent'"
                                        [color]="'theme'"
                                        (click)="reloadOptions()"
                                    >
                                        <i kbq-icon="kbq-arrow-rotate-left_16"></i>
                                        Повторить
                                    </button>
                                </kbq-select-error>
                            }
                            @case ('success') {
                                @for (option of state.data; track option) {
                                    <kbq-option [value]="option">
                                        <span
                                            [innerHTML]="option.label | kbqHighlightBackground: searchControl.value"
                                        ></span>
                                    </kbq-option>
                                }
                            }
                        }
                    }
                </kbq-select>
            </kbq-form-field>
        </div>
    `,
    styles: `
        .kbq-form-field {
            width: 320px;
        }
    `,
    providers: [SelectFacade, OptionsApiService],
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-column layout-align-center-center'
    }
})
export class SelectLoadingExample {
    protected readonly facade = inject(SelectFacade);

    protected selectedOption: Option = { id: 0, label: `Option #0` };

    protected readonly compareWith = (a: Option | null, b: Option | null) => a?.id === b?.id;
    protected readonly virtualOptionFactory = (value: Option) => new KbqVirtualOption(value, false, value.label);

    readonly state$ = this.facade.state$;
    readonly searchControl = new FormControl('');

    protected resetOptions() {
        // used as an example to show loading every time the select is opened
        this.facade.reset();
    }

    protected loadOptions() {
        // used as an example to show loading every time the select is opened
        this.searchControl.setValue('');
    }

    protected reloadOptions() {
        // used as an example to demonstrate a repeated request
        this.searchControl.setValue('');
    }

    constructor() {
        this.searchControl.valueChanges
            .pipe(takeUntilDestroyed())
            .subscribe((value) => this.facade.setSearch(value ?? ''));
    }
}
```

#### Loading Error

After the loading indicator, show an error message with an option to retry.

**Example: Select loading error** (`select-loading-error`)

```ts select-loading-error-example.ts
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqHighlightBackgroundPipe } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import {
    delayBeforeDisplayingResultWithoutOptions,
    KbqSelectModule,
    minimumTimeToDisplayLoading
} from '@koobiq/components/select';
import { BehaviorSubject, catchError, Observable, of, race, switchMap, throwError, timer } from 'rxjs';
import { map, startWith } from 'rxjs/operators';

type Option = {
    id: number;
    label: string;
};

export class OptionsApiService {
    private delay: number = 3000;

    private data = Array.from({ length: 10 }).map((_, i) => ({ id: i, label: `Option #${i}` }));

    setDelay(delay: number) {
        this.delay = delay;
    }

    getOptions(search: string): Observable<Option[]> {
        if (search === 'error') {
            return timer(this.delay).pipe(switchMap(() => throwError(() => new Error('error'))));
        }

        const term = search.toLowerCase().trim();

        const filtered = !term ? this.data : this.data.filter((option) => option.label.toLowerCase().includes(term));

        return timer(this.delay).pipe(map(() => filtered));
    }
}

export class SelectFacade {
    private readonly api = inject(OptionsApiService);
    private readonly search$ = new BehaviorSubject<string | null>(null);

    reset(): void {
        this.search$.next(null);
    }

    setSearch(value: string) {
        this.search$.next(value);
    }

    readonly state$ = this.search$.pipe(
        // for use in a real project
        // debounceTime(300),
        // distinctUntilChanged(),
        switchMap((search) => {
            if (search === null) {
                return of({ status: 'idle' } as const);
            }

            const request$ = this.api.getOptions(search).pipe(
                map((data) =>
                    data.length === 0 ? ({ status: 'empty' } as const) : ({ status: 'success', data } as const)
                ),
                catchError((error: any) => of({ status: 'error', error } as const))
            );

            return race(
                request$,
                timer(delayBeforeDisplayingResultWithoutOptions).pipe(
                    switchMap(() => {
                        return request$.pipe(
                            startWith({ status: 'loading' } as const),
                            switchMap((state) => {
                                if (state.status === 'loading') {
                                    return of(state);
                                }

                                return timer(minimumTimeToDisplayLoading).pipe(map(() => state));
                            })
                        );
                    })
                )
            );
        })
    );
}

@Component({
    selector: 'select-loading-error-example',
    imports: [
        KbqSelectModule,
        FormsModule,
        KbqIconModule,
        KbqInputModule,
        ReactiveFormsModule,
        AsyncPipe,
        KbqHighlightBackgroundPipe,
        KbqButtonModule
    ],
    template: `
        <div class="layout-column layout-align-center-start layout-gap-l">
            <kbq-form-field>
                <kbq-select (beforeOpened)="loadOptions()" (closed)="resetOptions()">
                    <kbq-form-field noBorders kbqSelectSearch>
                        <i kbq-icon="kbq-magnifying-glass_16" kbqPrefix></i>
                        <input kbqInput type="text" autocomplete="off" [formControl]="searchControl" />
                        <kbq-cleaner />
                    </kbq-form-field>

                    @if (state$ | async; as state) {
                        @switch (state.status) {
                            @case ('loading') {
                                <kbq-select-loading />
                            }
                            @case ('empty') {
                                <div kbq-select-search-empty-result>Ничего не найдено</div>
                            }
                            @case ('error') {
                                <kbq-select-error>
                                    <span kbq-select-error-text>Не удалось показать записи</span>
                                    <button
                                        kbq-button
                                        [kbqStyle]="'transparent'"
                                        [color]="'theme'"
                                        (click)="reloadOptions()"
                                    >
                                        <i kbq-icon="kbq-arrow-rotate-left_16"></i>
                                        Повторить
                                    </button>
                                </kbq-select-error>
                            }
                            @case ('success') {
                                @for (option of state.data; track option) {
                                    <kbq-option [value]="option">
                                        <span
                                            [innerHTML]="option.label | kbqHighlightBackground: searchControl.value"
                                        ></span>
                                    </kbq-option>
                                }
                            }
                        }
                    }
                </kbq-select>
            </kbq-form-field>
        </div>
    `,
    styles: `
        .kbq-form-field {
            width: 320px;
        }
    `,
    providers: [SelectFacade, OptionsApiService],
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-column layout-align-center-center'
    }
})
export class SelectLoadingErrorExample {
    protected readonly facade = inject(SelectFacade);

    readonly state$ = this.facade.state$;
    readonly searchControl = new FormControl('');

    protected resetOptions() {
        // used as an example to show loading every time the select is opened
        this.facade.reset();
    }

    protected loadOptions() {
        // used as an example to show loading every time the select is opened
        this.searchControl.setValue('error');
    }

    protected reloadOptions() {
        // used as an example to demonstrate a repeated request
        this.searchControl.setValue('');
    }

    constructor() {
        this.searchControl.valueChanges
            .pipe(takeUntilDestroyed())
            .subscribe((value) => this.facade.setSearch(value ?? ''));
    }
}
```

### Pagination Loading

**Example: Select paging** (`select-paging`)

```ts select-paging-example.ts
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqHighlightBackgroundPipe, KbqVirtualOption } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqProgressSpinnerModule } from '@koobiq/components/progress-spinner';
import {
    delayBeforeDisplayingResultWithoutOptions,
    KbqSelect,
    KbqSelectModule,
    minimumTimeToDisplayLoading
} from '@koobiq/components/select';
import {
    BehaviorSubject,
    catchError,
    exhaustMap,
    fromEvent,
    merge,
    Observable,
    of,
    race,
    Subject,
    Subscription,
    switchMap,
    timer
} from 'rxjs';
import { map, startWith, tap } from 'rxjs/operators';

type Option = {
    id: number;
    label: string;
};

type PagedResult = {
    data: Option[];
    hasMore: boolean;
};

type SelectState =
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'empty' }
    | { status: 'error'; error: any }
    | { status: 'success'; data: Option[]; hasMore: boolean; loadingMore: boolean };

const PAGE_SIZE = 20;
const DELAY = 1000;

export class OptionsApiService {
    private data = Array.from({ length: 100 }).map((_, i) => ({ id: i, label: `Option #${i}` }));

    getOptions(
        search: string,
        page: number = 0,
        delay: number = DELAY,
        pageSize: number = PAGE_SIZE
    ): Observable<PagedResult> {
        const term = search.toLowerCase().trim();

        const filtered = !term ? this.data : this.data.filter((option) => option.label.toLowerCase().includes(term));

        const start = page * pageSize;
        const end = start + pageSize;
        const pageData = filtered.slice(start, end);
        const hasMore = end < filtered.length;

        return timer(delay).pipe(map(() => ({ data: pageData, hasMore })));
    }
}

export class SelectFacade {
    private readonly api = inject(OptionsApiService);
    private readonly search$ = new BehaviorSubject<string | null>(null);
    readonly loadMore$ = new Subject<void>();

    private accumulated: Option[] = [];
    private currentPage = 0;
    private currentHasMore = false;
    private isLoadingMore = false;

    reset(): void {
        this.search$.next(null);
    }

    setSearch(value: string) {
        this.search$.next(value);
    }

    loadMore(): void {
        if (!this.currentHasMore || this.isLoadingMore) return;

        this.loadMore$.next();
    }

    readonly state$: Observable<SelectState> = this.search$.pipe(
        // for use in a real project
        // debounceTime(300),
        // distinctUntilChanged(),
        switchMap((search) => {
            if (search === null) {
                return of({ status: 'idle' } as SelectState);
            }

            this.accumulated = [];
            this.currentPage = 0;
            this.currentHasMore = false;
            this.isLoadingMore = false;

            const request$ = this.api.getOptions(search, 0, 0).pipe(
                map((result): SelectState => {
                    if (result.data.length === 0) {
                        return { status: 'empty' };
                    }

                    this.accumulated = result.data;
                    this.currentHasMore = result.hasMore;

                    return { status: 'success', data: this.accumulated, hasMore: result.hasMore, loadingMore: false };
                }),
                catchError((error: any) => of({ status: 'error', error } as SelectState))
            );

            const initialLoad$ = race(
                request$,
                timer(delayBeforeDisplayingResultWithoutOptions).pipe(
                    switchMap(() => {
                        return request$.pipe(
                            startWith({ status: 'loading' } as SelectState),
                            switchMap((state) => {
                                if (state.status === 'loading') {
                                    return of(state);
                                }

                                return timer(minimumTimeToDisplayLoading).pipe(map(() => state));
                            })
                        );
                    })
                )
            );

            const loadMore$ = this.loadMore$.pipe(
                exhaustMap(() => {
                    this.currentPage++;
                    this.isLoadingMore = true;

                    return this.api.getOptions(search, this.currentPage, DELAY).pipe(
                        map((result): SelectState => {
                            this.accumulated = [...this.accumulated, ...result.data];
                            this.currentHasMore = result.hasMore;
                            this.isLoadingMore = false;

                            return {
                                status: 'success',
                                data: this.accumulated,
                                hasMore: result.hasMore,
                                loadingMore: false
                            };
                        }),
                        startWith({
                            status: 'success',
                            data: this.accumulated,
                            hasMore: true,
                            loadingMore: true
                        } as SelectState)
                    );
                })
            );

            return merge(
                initialLoad$.pipe(
                    tap((state) => {
                        if (state.status === 'success') {
                            this.accumulated = state.data;
                        }
                    })
                ),
                loadMore$
            );
        })
    );
}

@Component({
    selector: 'select-paging-example',
    imports: [
        KbqSelectModule,
        KbqIconModule,
        KbqInputModule,
        ReactiveFormsModule,
        AsyncPipe,
        KbqHighlightBackgroundPipe,
        KbqButtonModule,
        KbqProgressSpinnerModule
    ],
    template: `
        <div class="layout-column layout-align-center-start layout-gap-l">
            <kbq-form-field>
                <kbq-select
                    #select
                    [compareWith]="compareWith"
                    [showPreselectedValues]="true"
                    [value]="selectedOption"
                    [virtualOptionFactory]="virtualOptionFactory"
                    (beforeOpened)="reloadOptions()"
                    (opened)="onSelectOpened(select)"
                    (closed)="onSelectClosed()"
                >
                    <kbq-form-field noBorders kbqSelectSearch>
                        <i kbq-icon="kbq-magnifying-glass_16" kbqPrefix></i>
                        <input kbqInput type="text" autocomplete="off" [formControl]="searchControl" />
                        <kbq-cleaner />
                    </kbq-form-field>

                    @if (facade.state$ | async; as state) {
                        @switch (state.status) {
                            @case ('loading') {
                                <kbq-select-loading />
                            }
                            @case ('empty') {
                                <div kbq-select-search-empty-result>Ничего не найдено</div>
                            }
                            @case ('error') {
                                <kbq-select-error>
                                    <span kbq-select-error-text>Не удалось показать записи</span>
                                    <button
                                        kbq-button
                                        [kbqStyle]="'transparent'"
                                        [color]="'theme'"
                                        (click)="reloadOptions()"
                                    >
                                        <i kbq-icon="kbq-arrow-rotate-left_16"></i>
                                        Повторить
                                    </button>
                                </kbq-select-error>
                            }
                            @case ('success') {
                                @for (option of state.data; track option.id) {
                                    <kbq-option [value]="option">
                                        <span
                                            [innerHTML]="option.label | kbqHighlightBackground: searchControl.value"
                                        ></span>
                                    </kbq-option>
                                }

                                @if (state.loadingMore) {
                                    <kbq-option [disabled]="true">
                                        <div class="layout-row layout-align-start-center">
                                            <kbq-progress-spinner [mode]="'indeterminate'" />
                                            <span class="layout-padding-left-s">Загрузка</span>
                                        </div>
                                    </kbq-option>
                                }
                            }
                        }
                    }
                </kbq-select>
            </kbq-form-field>
        </div>
    `,
    styles: `
        .kbq-form-field {
            width: 320px;
        }
    `,
    providers: [SelectFacade, OptionsApiService],
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-column layout-align-center-center'
    }
})
export class SelectPagingExample implements OnDestroy {
    protected readonly facade = inject(SelectFacade);

    protected selectedOption: Option = { id: 0, label: `Option #0` };

    protected readonly compareWith = (a: Option | null, b: Option | null) => a?.id === b?.id;
    protected readonly virtualOptionFactory = (value: Option) => new KbqVirtualOption(value, false, value.label);

    readonly searchControl = new FormControl('');

    private scrollSub: Subscription | null = null;

    protected onSelectOpened(select: KbqSelect) {
        const scrollContainer = select.optionsContainer()?.nativeElement;

        if (!scrollContainer) return;

        this.scrollSub = fromEvent(scrollContainer, 'scroll').subscribe(() => {
            const { scrollTop, scrollHeight, clientHeight } = scrollContainer;

            if (scrollTop + clientHeight >= scrollHeight - 30) {
                this.facade.loadMore();
            }
        });
    }

    protected onSelectClosed() {
        this.scrollSub?.unsubscribe();
        this.scrollSub = null;
        this.facade.reset();
    }

    // in beforeOpened used as an example to show loading every time the select is opened
    protected reloadOptions() {
        this.searchControl.setValue('');
    }

    ngOnDestroy() {
        this.scrollSub?.unsubscribe();
    }

    constructor() {
        this.searchControl.valueChanges
            .pipe(takeUntilDestroyed())
            .subscribe((value) => this.facade.setSearch(value ?? ''));
    }
}
```

#### Pagination Loading Error

**Example: Select paging error** (`select-paging-error`)

```ts select-paging-error-example.ts
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, OnDestroy } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqHighlightBackgroundPipe, KbqVirtualOption } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqProgressSpinnerModule } from '@koobiq/components/progress-spinner';
import {
    delayBeforeDisplayingResultWithoutOptions,
    KbqSelect,
    KbqSelectModule,
    minimumTimeToDisplayLoading
} from '@koobiq/components/select';
import {
    BehaviorSubject,
    catchError,
    exhaustMap,
    fromEvent,
    merge,
    Observable,
    of,
    race,
    Subject,
    Subscription,
    switchMap,
    throwError,
    timer
} from 'rxjs';
import { map, startWith, tap } from 'rxjs/operators';

type Option = {
    id: number;
    label: string;
};

type PagedResult = {
    data: Option[];
    hasMore: boolean;
};

type SelectState =
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'empty' }
    | { status: 'error'; error: any }
    | { status: 'success'; data: Option[]; pageNotLoaded?: boolean; hasMore: boolean; loadingMore: boolean };

const PAGE_SIZE = 20;
const DELAY = 1000;

export class OptionsApiService {
    private data = Array.from({ length: 100 }).map((_, i) => ({ id: i, label: `Option #${i}` }));

    getOptions(
        search: string,
        page: number = 0,
        delay: number = DELAY,
        error: boolean = false,
        pageSize: number = PAGE_SIZE
    ): Observable<PagedResult> {
        if (error) {
            return timer(DELAY).pipe(switchMap(() => throwError(() => new Error('error'))));
        }

        const term = search.toLowerCase().trim();

        const filtered = !term ? this.data : this.data.filter((option) => option.label.toLowerCase().includes(term));

        const start = page * pageSize;
        const end = start + pageSize;
        const pageData = filtered.slice(start, end);
        const hasMore = end < filtered.length;

        return timer(delay).pipe(map(() => ({ data: pageData, hasMore })));
    }
}

export class SelectFacade {
    private readonly api = inject(OptionsApiService);
    private readonly search$ = new BehaviorSubject<string | null>(null);
    readonly loadMore$ = new Subject<void>();

    private accumulated: Option[] = [];
    private currentPage = 0;
    private currentHasMore = false;
    private isLoadingMore = false;

    reset(): void {
        this.search$.next(null);
    }

    setSearch(value: string) {
        this.search$.next(value);
    }

    loadMore(): void {
        if (!this.currentHasMore || this.isLoadingMore) return;

        this.loadMore$.next();
    }

    readonly state$: Observable<SelectState> = this.search$.pipe(
        // for use in a real project
        // debounceTime(300),
        // distinctUntilChanged(),
        switchMap((search) => {
            if (search === null) {
                return of({ status: 'idle' } as SelectState);
            }

            this.accumulated = [];
            this.currentPage = 0;
            this.currentHasMore = false;
            this.isLoadingMore = false;

            const request$ = this.api.getOptions(search, 0, 0).pipe(
                map((result): SelectState => {
                    if (result.data.length === 0) {
                        return { status: 'empty' };
                    }

                    this.accumulated = result.data;
                    this.currentHasMore = result.hasMore;

                    return { status: 'success', data: this.accumulated, hasMore: result.hasMore, loadingMore: false };
                }),
                catchError((error: any) => of({ status: 'error', error } as SelectState))
            );

            const initialLoad$ = race(
                request$,
                timer(delayBeforeDisplayingResultWithoutOptions).pipe(
                    switchMap(() => {
                        return request$.pipe(
                            startWith({ status: 'loading' } as SelectState),
                            switchMap((state) => {
                                if (state.status === 'loading') {
                                    return of(state);
                                }

                                return timer(minimumTimeToDisplayLoading).pipe(map(() => state));
                            })
                        );
                    })
                )
            );

            const loadMore$ = this.loadMore$.pipe(
                exhaustMap(() => {
                    this.currentPage++;
                    this.isLoadingMore = true;

                    return this.api.getOptions(search, this.currentPage, DELAY, this.currentPage > 1).pipe(
                        map((result): SelectState => {
                            this.accumulated = [...this.accumulated, ...result.data];
                            this.currentHasMore = result.hasMore;
                            this.isLoadingMore = false;

                            return {
                                status: 'success',
                                data: this.accumulated,
                                hasMore: result.hasMore,
                                loadingMore: false
                            };
                        }),
                        catchError(() => {
                            this.currentPage--;
                            this.isLoadingMore = false;
                            this.currentHasMore = false;

                            return of({
                                status: 'success',
                                data: this.accumulated,
                                hasMore: this.currentHasMore,
                                loadingMore: this.isLoadingMore,
                                pageNotLoaded: true
                            } as SelectState);
                        }),
                        startWith({
                            status: 'success',
                            data: this.accumulated,
                            hasMore: true,
                            loadingMore: true
                        } as SelectState)
                    );
                })
            );

            return merge(
                initialLoad$.pipe(
                    tap((state) => {
                        if (state.status === 'success') {
                            this.accumulated = state.data;
                        }
                    })
                ),
                loadMore$
            );
        })
    );
}

@Component({
    selector: 'select-paging-error-example',
    imports: [
        KbqSelectModule,
        KbqIconModule,
        KbqInputModule,
        ReactiveFormsModule,
        AsyncPipe,
        KbqHighlightBackgroundPipe,
        KbqButtonModule,
        KbqProgressSpinnerModule
    ],
    template: `
        <div class="layout-column layout-align-center-start layout-gap-l">
            <kbq-form-field>
                <kbq-select
                    #select
                    [compareWith]="compareWith"
                    [showPreselectedValues]="true"
                    [value]="selectedOption"
                    [virtualOptionFactory]="virtualOptionFactory"
                    (beforeOpened)="reloadOptions()"
                    (opened)="onSelectOpened(select)"
                    (closed)="onSelectClosed()"
                >
                    <kbq-form-field noBorders kbqSelectSearch>
                        <i kbq-icon="kbq-magnifying-glass_16" kbqPrefix></i>
                        <input kbqInput type="text" autocomplete="off" [formControl]="searchControl" />
                        <kbq-cleaner />
                    </kbq-form-field>

                    @if (facade.state$ | async; as state) {
                        @switch (state.status) {
                            @case ('loading') {
                                <kbq-select-loading />
                            }
                            @case ('empty') {
                                <div kbq-select-search-empty-result>Ничего не найдено</div>
                            }
                            @case ('error') {
                                <kbq-select-error>
                                    <span kbq-select-error-text>Не удалось показать записи</span>
                                    <button
                                        kbq-button
                                        [kbqStyle]="'transparent'"
                                        [color]="'theme'"
                                        (click)="reloadOptions()"
                                    >
                                        <i kbq-icon="kbq-arrow-rotate-left_16"></i>
                                        Повторить
                                    </button>
                                </kbq-select-error>
                            }
                            @case ('success') {
                                @for (option of state.data; track option.id) {
                                    <kbq-option [value]="option">
                                        <span
                                            [innerHTML]="option.label | kbqHighlightBackground: searchControl.value"
                                        ></span>
                                    </kbq-option>
                                }

                                @if (state.loadingMore) {
                                    <kbq-option [disabled]="true">
                                        <div class="layout-row layout-align-start-center">
                                            <kbq-progress-spinner [mode]="'indeterminate'" />
                                            <span class="layout-padding-left-s">Загрузка</span>
                                        </div>
                                    </kbq-option>
                                }

                                @if (state.pageNotLoaded) {
                                    <kbq-select-error paging>
                                        <span kbq-select-error-text>Не удалось показать записи</span>
                                        <div>
                                            <button
                                                kbq-button
                                                [kbqStyle]="'transparent'"
                                                [color]="'theme'"
                                                (click)="reloadOptions()"
                                            >
                                                <i kbq-icon="kbq-arrow-rotate-left_16"></i>
                                                Повторить
                                            </button>
                                        </div>
                                    </kbq-select-error>
                                }
                            }
                        }
                    }
                </kbq-select>
            </kbq-form-field>
        </div>
    `,
    styles: `
        .kbq-form-field {
            width: 320px;
        }
    `,
    providers: [SelectFacade, OptionsApiService],
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-column layout-align-center-center'
    }
})
export class SelectPagingErrorExample implements OnDestroy {
    protected readonly facade = inject(SelectFacade);

    protected selectedOption: Option = { id: 0, label: `Option #0` };

    protected readonly compareWith = (a: Option | null, b: Option | null) => a?.id === b?.id;
    protected readonly virtualOptionFactory = (value: Option) => new KbqVirtualOption(value, false, value.label);

    readonly searchControl = new FormControl('');

    private scrollSub: Subscription | null = null;

    protected onSelectOpened(select: KbqSelect) {
        const scrollContainer = select.optionsContainer()?.nativeElement;

        if (!scrollContainer) return;

        this.scrollSub = fromEvent(scrollContainer, 'scroll').subscribe(() => {
            const { scrollTop, scrollHeight, clientHeight } = scrollContainer;

            if (scrollTop + clientHeight >= scrollHeight - 30) {
                this.facade.loadMore();
            }
        });
    }

    protected onSelectClosed() {
        this.scrollSub?.unsubscribe();
        this.scrollSub = null;
        this.facade.reset();
    }

    // in beforeOpened used as an example to show loading every time the select is opened
    protected reloadOptions() {
        this.searchControl.setValue('');
    }

    ngOnDestroy() {
        this.scrollSub?.unsubscribe();
    }

    constructor() {
        this.searchControl.valueChanges
            .pipe(takeUntilDestroyed())
            .subscribe((value) => this.facade.setSearch(value ?? ''));
    }
}
```

### No Available Options

A select component is intended to have available choices. If it has no options, this should be treated as an exceptional case. When the application does not provide any choices, the select should be replaced with an input field or a dialog for creating a new entity.

**Example: Select add new option** (`select-add-new-option`)

```ts select-add-new-option-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqForm, KbqFormElement } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqLink, KbqLinkPrefix } from '@koobiq/components/link';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-add-new-option-example',
    imports: [
        KbqSelectModule,
        FormsModule,
        KbqIconModule,
        KbqButtonModule,
        KbqForm,
        KbqFormElement,
        KbqLink,
        KbqLinkPrefix
    ],
    template: `
        <form novalidate class="kbq-form-vertical">
            <div class="kbq-form__row">
                <label class="kbq-form__label">Login account</label>
                <kbq-form-field class="kbq-form__control">
                    <kbq-select [value]="selected">
                        @for (option of options; track option) {
                            <kbq-option [value]="option">{{ option }}</kbq-option>
                        }
                    </kbq-select>
                </kbq-form-field>
            </div>

            <div class="kbq-form__row">
                <div>Account for privilege escalation</div>
                <div class="layout-padding-top-3xs" [style.color]="'var(--kbq-foreground-contrast-secondary)'">
                    There are no suitable accounts of this type (Terminal) in Mega App
                </div>
                <div class="layout-padding-top-xs">
                    <span kbqLink>
                        <i kbq-icon="kbq-plus_16" kbqLinkPrefix></i>
                        New Account
                    </span>
                </div>
            </div>
        </form>
    `,
    styles: `
        .kbq-form-vertical {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-column layout-align-center-center'
    }
})
export class SelectAddNewOptionExample {
    readonly options = [
        'admin / @dm1n',
        'root / P@ssw0rd',
        'user / us3R'
    ];
    readonly selected = this.options[1];
}
```

As a last resort, you can place a message in the dropdown menu. Hide the search field if it is known for sure that there are no options in the menu at all.

**Example: Select no variants** (`select-no-variants`)

```ts select-no-variants-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqForm, KbqFormElement } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-no-variants-example',
    imports: [
        KbqSelectModule,
        FormsModule,
        KbqIconModule,
        KbqForm,
        KbqFormElement,
        KbqButtonModule
    ],
    template: `
        <form novalidate class="kbq-form-vertical">
            <div class="kbq-form__row">
                <label class="kbq-form__label">Empty select</label>
                <kbq-form-field>
                    <kbq-select>
                        @for (option of options; track option) {
                            <kbq-option [value]="option">{{ option }}</kbq-option>
                        }

                        @if (noOptions) {
                            <kbq-select-no-options>Нет вариантов выбора</kbq-select-no-options>
                        }
                    </kbq-select>
                </kbq-form-field>
            </div>
        </form>
    `,
    styles: `
        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-column layout-align-center-center'
    }
})
export class SelectNoVariantsExample {
    options: string[];
    noOptions: boolean;

    constructor() {
        this.loadOptions();
    }

    loadOptions() {
        this.options = [];
        this.noOptions = true;
    }
}
```

### Custom Message Design

**Example: Select loading error (custom)** (`select-loading-error-custom`)

```ts select-loading-error-custom-example.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqForm, KbqFormElement } from '@koobiq/components/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqSelectModule } from '@koobiq/components/select';

const delayBeforeShowError = 3000;

@Component({
    selector: 'select-loading-error-custom-example',
    imports: [
        KbqSelectModule,
        FormsModule,
        KbqIconModule,
        KbqForm,
        KbqFormElement,
        KbqButtonModule,
        KbqEmptyStateModule
    ],
    template: `
        <form novalidate class="kbq-form-vertical">
            <div class="kbq-form__row">
                <label class="kbq-form__label">Custom Error State</label>
                <kbq-form-field>
                    <kbq-select (opened)="loadOptions()" (closed)="reset()">
                        @if (loading()) {
                            <kbq-select-loading />
                        }

                        @if (error()) {
                            <kbq-empty-state class="layout-margin-top-l layout-margin-bottom-3xl" [errorColor]="true">
                                <i
                                    kbq-empty-state-icon
                                    kbq-icon-item="kbq-triangle-exclamation_16"
                                    [big]="true"
                                    [color]="'contrast'"
                                    [fade]="true"
                                ></i>
                                <div kbq-empty-state-text>Failed to load the location list</div>
                                <div kbq-empty-state-actions>
                                    <button kbq-button [color]="'theme'" [kbqStyle]="'transparent'">Try Again</button>
                                </div>
                            </kbq-empty-state>
                        }
                    </kbq-select>
                </kbq-form-field>
            </div>
        </form>
    `,
    styles: `
        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    host: {
        class: 'layout-margin-5xl layout-column layout-align-center-center'
    }
})
export class SelectLoadingErrorCustomExample {
    readonly error = signal(false);
    readonly loading = signal(true);

    protected loadOptions() {
        setTimeout(() => {
            this.loading.set(false);
            this.error.set(true);
        }, delayBeforeShowError);
    }

    protected reset() {
        this.loading.set(true);
        this.error.set(false);
    }
}
```

#### Example with kbq-select-trigger

kbq-select-trigger allows you to override the block that displays the selected value without affecting the placeholder, cleaner, or icon.

**Example: select-custom-trigger** (`select-custom-trigger`)

```ts select-custom-trigger-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-custom-trigger-example',
    imports: [KbqSelectModule],
    template: `
        <kbq-select #select="kbqSelect" [(value)]="selected">
            <ng-container kbq-select-trigger>custom trigger {{ select.triggerValue }}</ng-container>

            @for (option of options; track option) {
                <kbq-option [value]="option">{{ option }}</kbq-option>
            }
        </kbq-select>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectCustomTriggerExample {
    readonly options = Array.from({ length: 5 }).map((_, i) => `Option #${i}`);
    readonly selected = this.options[0];
}
```

#### Example with kbq-select-matcher

kbq-select-matcher allows you to replace the entire contents of kbq-select, including the placeholder, cleaner, and icon.

**Example: select-custom-matcher** (`select-custom-matcher`)

```ts select-custom-matcher-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-custom-matcher-example',
    imports: [KbqSelectModule, KbqButtonModule],
    template: `
        <kbq-select
            #select="kbqSelect"
            [tabIndex]="-1"
            [class]="{ 'kbq-select': false, 'my-custom-select': true }"
            [(value)]="selected"
        >
            <button kbq-button kbq-select-matcher>
                {{ select.triggerValue }}

                <i class="layout-padding-left-3xs" kbq-icon="kbq-chevron-down-s_16"></i>
            </button>

            @for (option of options; track option) {
                <kbq-option [value]="option">{{ option }}</kbq-option>
            }
        </kbq-select>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .my-custom-select {
            outline: none;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectCustomMatcherExample {
    readonly options = Array.from({ length: 5 }).map((_, i) => `Option #${i}`);
    readonly selected = this.options[0];
}
```

#### Example with kbqSelectTagContent

`kbqSelectTagContent` allows you to override the markup of every tag in the matcher in multiple selection mode. The template receives the option itself and the select instance, so each tag can be rendered differently depending on its value.

**Example: select-custom-tag-content** (`select-custom-tag-content`)

```ts select-custom-tag-content-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqComponentColors } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqSelectModule } from '@koobiq/components/select';
import { KbqTagsModule } from '@koobiq/components/tags';

@Component({
    selector: 'select-custom-tag-content-example',
    imports: [KbqSelectModule, KbqTagsModule, KbqIconModule],
    template: `
        <kbq-form-field>
            <kbq-select multiple placeholder="Placeholder" [(value)]="selected">
                @for (name of severityNames; track name) {
                    <kbq-option [value]="name">{{ name }}</kbq-option>
                }

                <ng-template #kbqSelectTagContent let-option let-select="select">
                    <kbq-tag
                        [selectable]="false"
                        [disabled]="option.disabled || select.disabled()"
                        [color]="severities[option.value].color"
                    >
                        <i kbqTagPrefix [kbq-icon]="severities[option.value].icon"></i>
                        {{ option.viewValue }}
                        <!-- The custom template replaces the built-in markup, so the remove control is up to us. -->
                        @if (!option.disabled && !select.disabled()) {
                            <i
                                kbq-icon="kbq-xmark-s_16"
                                kbqTagRemove
                                (click)="select.onRemoveMatcherItem(option, $event)"
                            ></i>
                        }
                    </kbq-tag>
                </ng-template>

                <kbq-cleaner #kbqSelectCleaner />
            </kbq-select>
        </kbq-form-field>
    `,
    styles: `
        :host {
            display: flex;
            justify-content: center;
            padding: var(--kbq-size-l);
        }

        .kbq-form-field {
            width: 320px;
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectCustomTagContentExample {
    /**
     * Per-value tag appearance — the built-in markup paints every tag with the same color.
     * Only these four colors are themed for `kbq-tag`.
     */
    readonly severities: Record<string, { color: KbqComponentColors; icon: string }> = {
        Critical: { color: KbqComponentColors.Error, icon: 'kbq-circle-xmark_16' },
        Major: { color: KbqComponentColors.Warning, icon: 'kbq-triangle-exclamation_16' },
        Minor: { color: KbqComponentColors.Theme, icon: 'kbq-circle-info_16' },
        Info: { color: KbqComponentColors.ContrastFade, icon: 'kbq-circle-info_16' }
    };

    /** List of options — the keys of `severities`, computed once so the template doesn't rebuild it. */
    readonly severityNames = Object.keys(this.severities);

    /** Selected values; not readonly, because the field is bound with `[(value)]`. */
    selected = this.severityNames.slice(0, 3);
}
```

### Custom scroll strategy

Override the scroll strategy via `KBQ_SELECT_SCROLL_STRATEGY`. `KbqAutoHideScrollStrategy` (from `@koobiq/components/core`) stops repositioning once the trigger scrolls out of view and emits on `hide`, so you can close the panel yourself.

**Example: select-auto-hide-scroll-strategy** (`select-auto-hide-scroll-strategy`)

```ts select-auto-hide-scroll-strategy-example.ts
import { CdkScrollableModule, ScrollDispatcher, ViewportRuler } from '@angular/cdk/scrolling';
import {
    AfterViewInit,
    ChangeDetectionStrategy,
    Component,
    DestroyRef,
    NgZone,
    inject,
    viewChild
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
    KBQ_SELECT_SCROLL_STRATEGY,
    KbqAutoHideScrollStrategy,
    kbqAutoHideScrollStrategyFactory
} from '@koobiq/components/core';
import { KbqNativeScrollbar } from '@koobiq/components/scrollbar';
import { KbqSelect, KbqSelectModule } from '@koobiq/components/select';

@Component({
    selector: 'select-auto-hide-scroll-strategy-example',
    imports: [CdkScrollableModule, KbqSelectModule, KbqNativeScrollbar],
    template: `
        <div class="example-select-auto-hide-scroll-strategy__container" kbqNativeScrollbar cdkScrollable>
            <div class="example-select-auto-hide-scroll-strategy__spacer">Scroll down</div>

            <kbq-form-field>
                <kbq-select [value]="selected">
                    @for (option of options; track option) {
                        <kbq-option [value]="option">{{ option }}</kbq-option>
                    }
                </kbq-select>
            </kbq-form-field>

            <div class="example-select-auto-hide-scroll-strategy__spacer">Scroll up</div>
        </div>
    `,
    styles: `
        .example-select-auto-hide-scroll-strategy__container {
            height: 200px;
            overflow-y: auto;
            border: 1px solid var(--kbq-line-contrast-less);
            border-radius: 4px;
            padding: 16px;
        }

        .example-select-auto-hide-scroll-strategy__spacer {
            display: flex;
            align-items: center;
            justify-content: center;
            height: 250px;
            color: var(--kbq-foreground-contrast-secondary);
        }
    `,
    providers: [
        {
            provide: KBQ_SELECT_SCROLL_STRATEGY,
            useFactory: () => {
                const scrollDispatcher = inject(ScrollDispatcher);
                const viewportRuler = inject(ViewportRuler);
                const ngZone = inject(NgZone);

                return kbqAutoHideScrollStrategyFactory(scrollDispatcher, viewportRuler, ngZone);
            }
        }
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SelectAutoHideScrollStrategyExample implements AfterViewInit {
    protected readonly options = Array.from({ length: 5 }).map((_, i) => `Option #${i}`);
    protected readonly selected = this.options[0];

    private readonly select = viewChild.required(KbqSelect);
    private readonly destroyRef = inject(DestroyRef);

    ngAfterViewInit(): void {
        const select = this.select();

        // `scrollStrategy` is created eagerly as a field initializer, so it's ready to subscribe
        // to as soon as the view is initialized — no need to force-create an overlay first.
        const scrollStrategy = select.scrollStrategy as KbqAutoHideScrollStrategy;

        scrollStrategy.hide.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => select.close());
    }
}
```
