# Select

> Native-like select built on CDK Overlay. A signal-forms native control — it implements `FormValueControl`, so `[formField]` binds straight to its `value` model. `[(value)]` works standalone, and `[(ngModel)]` / reactive forms keep working through Angular's bridge.

Source: https://ngwr.dev/reference/components/select  
Kind: Component, Standalone, CDK Overlay

## Installation

```angular-ts
import { WrSelect, WrOption } from 'ngwr/select';
import { FormsModule } from '@angular/forms';

@Component({ imports: [WrSelect, WrOption, FormsModule] })
export class MyComponent {}
```

## Basic usage

```html
<wr-select placeholder="Pick a size" [(value)]="size">
  <wr-option value="sm">Small</wr-option>
  <wr-option value="md">Medium</wr-option>
  <wr-option value="lg">Large</wr-option>
</wr-select>
```

## Responsive (bottom-sheet)

With `responsive` — or app-wide via `provideWrResponsiveOverlays()` — the option panel detaches from the trigger and slides up as a full-width bottom-sheet on small viewports, with a backdrop. Open this on a phone (or narrow the window below 640px) to see it dock to the bottom.

```html
<wr-select responsive placeholder="Pick a size">…</wr-select>
```

## Groups

Use \<wr-option-group> to label sections of related options.

```html
<wr-select [(value)]="framework">
  <wr-option-group label="Frontend">
    <wr-option value="angular">Angular</wr-option>
    <wr-option value="react">React</wr-option>
    <wr-option value="vue">Vue</wr-option>
  </wr-option-group>
  <wr-option-group label="Backend">
    <wr-option value="nest">NestJS</wr-option>
    <wr-option value="fastify">Fastify</wr-option>
  </wr-option-group>
</wr-select>
```

## Disabled

```html
<wr-select placeholder="Disabled" disabled />
```

## Multi mode

Set the `mode` input to `'multi'` to switch the model to `T[]`. Each pick renders as a chip with a remove button; the panel stays open while you keep picking. Backspace on the closed trigger removes the last chip. A clear-all (×) appears once at least one chip is selected.

```html
<wr-select mode="multi" placeholder="Pick tags" [(value)]="tags">
  <wr-option value="typescript">TypeScript</wr-option>
  <wr-option value="angular">Angular</wr-option>
  <wr-option value="rxjs">RxJS</wr-option>
  <wr-option value="signals">Signals</wr-option>
</wr-select>
```

## Chip overflow + max items

`[maxTagCount]` collapses extra chips into a `+N more` indicator on the trigger so long selections don't wrap forever. `[maxItems]` caps the underlying array.

```html
<wr-select mode="multi" [maxTagCount]="2" [maxItems]="6" [(value)]="manyTags">
  <wr-option value="typescript">TypeScript</wr-option>
  <wr-option value="angular">Angular</wr-option>
  <wr-option value="rxjs">RxJS</wr-option>
  <wr-option value="signals">Signals</wr-option>
  <wr-option value="cdk">CDK</wr-option>
  <wr-option value="ssr">SSR</wr-option>
</wr-select>
```

## Search mode

The `search` mode turns the trigger into a text input that filters the projected `<wr-option>` children case-insensitively. With `clearable`, Backspace on an empty field unsets the value — the × was otherwise the only way to clear one here, which made it a mouse-only action. Replaces the removed `wr-autocomplete` for the common sync-filter case.

```html
<wr-select mode="search" placeholder="Search a country" [(value)]="country">
  @for (c of countries; track c) {
    <wr-option [value]="c">{{ c }}</wr-option>
  }
</wr-select>
```

## Searchable multi-select

`[mode]` picks one shape, so `searchable` exists alongside it: on `multi` the trigger keeps its chips and grows an inline text field. Type to filter, Enter to toggle the highlighted option, Backspace on an empty query to drop the last chip. The panel stays open so you can pick several matches without retyping.

```html
<!-- searchable is orthogonal to mode — this is multi + typeahead. -->
<wr-select mode="multi" searchable placeholder="Filter categories" [(value)]="categories">
  @for (c of allCategories; track c) {
    <wr-option [value]="c">{{ c }}</wr-option>
  }
</wr-select>
```

## Server-side search

`(searchChange)` is the hook when the options live somewhere else: it emits the debounced query, you dispatch, and the results come back through `[options]` with `[loading]` driving the panel's progress row. Add `serverSearch` so the built-in client-side filter stays out of the way — otherwise a ranked or fuzzy match whose label doesn't contain the query would be hidden again. The demo below fakes 700 ms of latency.

```angular-html
<!-- Options live in the store; (searchChange) is the only wiring. -->
<wr-select
  mode="search"
  serverSearch
  placeholder="Search a country"
  [options]="results()"
  [loading]="pending()"
  [(value)]="picked"
  (searchChange)="onSearch($event)"
/>
```

```typescript
// (searchChange) is debounced by [debounceMs] and fires on every settled
// change — including '' when the field is cleared, so the store can reset.
onSearch(query: string): void {
  this.store.dispatch(searchCountries({ query }));
}

// results()/pending() are plain store selectors.
```

## Search mode — virtual scroll

For large `[options]` arrays, add `virtualScroll` so only ~one viewport of rows stays in the DOM. It engages on the data-array path only (falls back to the full render when static `<wr-option>` children are projected); rows are navigated via `aria-activedescendant` and selected by value. Type to filter, Arrow / Home / End / Enter to pick. The list below holds 5,000 items.

```angular-html
<!-- 5,000 options via the [options] data array + virtualScroll:
     only ~one viewport of rows is ever in the DOM. -->
<wr-select
  mode="search"
  virtualScroll
  placeholder="Search 5,000 items"
  [options]="bigOptions"
  [(value)]="picked"
/>
```

## Tag mode

The `tag` mode turns the trigger into a chip area with an inline input. Press one of `[separators]` (default: Enter or comma) to commit the draft. Backspace on an empty input removes the last chip. Replaces the removed `wr-chips-input`.

```html
<wr-select mode="tag" placeholder="Add a tag" [(value)]="tags" />

<!-- With separators + validator + caps -->
<wr-select
  mode="tag"
  placeholder="Add email and press Enter or ,"
  [(value)]="recipients"
  [separators]="['Enter', ',', ' ']"
  [validate]="isEmail"
  [maxItems]="5"
/>
```

## Select API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `placeholder` | Placeholder shown when no option is selected. Falls back to `select.placeholder`. | `string \| null` | `null` |
| `clearLabel` | Clear-selection (×) button aria-label. Falls back to `select.clearSelection`. | `string \| null` | `null` |
| `ariaLabel` | Accessible name of the trigger. Falls back to the placeholder, then to `select.label` — `role="combobox"` on an empty trigger otherwise has no name at all, which is the common case before anything is selected. | `string \| null` | `null` |
| `disabled` | Disable the select. Bound automatically from the field's disabled state when used with `[formField]`. | `boolean` | `false` |
| `rounded` | Pill-shaped corners on the trigger. Unset falls back to the `select.rounded` app default from `provideWrConfig()`; `[rounded]="false"` turns a configured `true` back off. | `boolean \| null` | `false` |
| `size` | Control size — shares the `--wr-control-*` contract. Unset falls back to the `select.size` app default from `provideWrConfig()`. | `WrSelectSize \| null` | `'md'` |
| `responsive` | Present the option panel as a full-width bottom-sheet on small viewports instead of an anchored dropdown. `undefined` follows the app-wide `provideWrResponsiveOverlays()` setting; `true`/`false` overrides it. | `boolean \| undefined` | `undefined` |
| `mode` | Behavior mode. `<wr-select>` is the unified combobox primitive — pick the shape via `[mode]`: - `'single'` (default) — one value, no input. Classic dropdown. - `'multi'` — array value, chips on the trigger. - `'search'` — type-ahead with sync filter or async loader. - `'tag'` — free-text + chips. | `WrSelectMode \| null` | `null` |
| `searchable` | Add a type-ahead filter without changing the value shape — the missing multi-with-search combination, since `[mode]` picks exactly one shape: ```html <wr-select mode="multi" searchable [(value)]="categories">…</wr-select>```On`multi`the trigger keeps its chips and grows an inline text field (the shape`tag`mode already uses); on`single`it is equivalent to`mode="search"`. Every search input (`[options]`,`[loader]`,`[debounceMs]`,`[minChars]`,`[virtualScroll]`, the`(searchChange)`output, …) applies either way. Ignored in`tag` mode, which owns its own input. | `boolean` | `false` |
| `searchQuery` | The live search query. Empty string = no filter. Exposed via context so each `<wr-option>` can self-hide non-matching rows. Two-way bindable, so the query can be owned or reset from outside: `[(searchQuery)]="query"`. For server-side search prefer the debounced {@link searchChange} output — `searchQueryChange` fires on every keystroke. | `string` | `''` |
| `options` | Search mode: dynamic option array. Each item is rendered as a `<wr-option>` whose label comes from `[displayWith]`. Works alongside projected `<wr-option>` children — both lists are filtered by the search query. | `readonly unknown[]` | `[]` |
| `displayWith` | Search mode: map a dynamic option item to its display label. | `(item: unknown) => string` | `String` |
| `loader` | Search mode: async loader. When set, the loader is called on every (debounced) keystroke and its result replaces `[options]`. Supports Observables, Promises, and plain arrays. | `WrSelectSearchLoader<unknown> \| null` | `null` |
| `serverSearch` | The option list is already scoped to the query upstream — skip the built-in client-side label filter. Set it when `[options]` is fed from a server via the {@link searchChange} output; the async `[loader]` path implies it. Without this, a server that ranks or fuzzy-matches (returning rows whose labels don't literally contain the query) would have those rows hidden again on the client. | `boolean` | `false` |
| `debounceMs` | Search mode: debounce (ms) applied to the loader. | `number` | `250` |
| `minChars` | Search mode: minimum query length before the panel opens / loader fires. | `number` | `0` |
| `freeText` | Search mode: allow values not in the options list. Enter on an unmatched query commits the raw text as the form value. | `boolean` | `false` |
| `virtualScroll` | Search mode: window the option panel so thousands of `[options]` keep only ~one viewport of rows in the DOM. Opt-in and OFF by default. Engages ONLY on the pure data-array search tier — it silently falls back to the full render when static `<wr-option>` children are projected or the filtered list is empty. While on, rows are plain `role="option"` elements (no `<wr-option>`), navigated via `aria-activedescendant`, selected by value. | `boolean` | `false` |
| `rowHeight` | Search mode: uniform option-row height in px for the virtual window. `0` (default) measures the first rendered row once and reuses it, so it adapts to the control size. Read only when `virtualScroll` engages. | `number` | `0` |
| `viewportHeight` | Search mode: height of the virtual scroll viewport — a number (px) or any CSS length. | `number \| string` | `256` |
| `overscan` | Search mode: extra rows kept above/below the virtual viewport. | `number` | `6` |
| `noResultsText` | Search mode: text shown when the filter / loader returns nothing. Falls back to `select.noResults`. | `string \| null` | `null` |
| `loadingText` | Search mode: text shown while the async loader is in flight. Falls back to `select.loading`. | `string \| null` | `null` |
| `(searchChange)` | Debounced search query, gated to searchable selects. Emits on every settled change — including the empty string when the field is cleared — so a store-backed option list can stay in sync and reset itself. `[debounceMs]` controls the delay; `[minChars]` does NOT gate it, for the same reason the `[loader]` path clears its results below that threshold. This is the hook for server-side search: dispatch on `(searchChange)`, feed the results back through `[options]` (plus `[serverSearch]`), and drive the panel's progress row with `[loading]`. Prefer it over the model's raw `(searchQueryChange)`, which fires on every keystroke. | `string` | — |
| `loading` | Show the panel's progress row. Independent of `[loader]`, which raises and lowers its own flag — use this when the options come from a store fed by the {@link searchChange} output. | `boolean` | `false` |
| `separators` | Tag mode: keys / characters that commit the current draft into a chip. `'Enter'` is the key name; everything else is a literal character watched in keypresses and pastes. | `readonly string[]` | `['Enter', ',']` |
| `allowDuplicates` | Tag mode: allow the same value to appear more than once. | `boolean` | `false` |
| `validate` | Tag mode: custom validator — return `true` to accept the value, `false` to silently reject. Receives the trimmed draft + the existing chips. | `WrSelectTagValidator \| null` | `null` |
| `clearable` | Show a clear-all (×) button at the end of the chip row when at least one option is selected (multi mode only). | `boolean` | `true` |
| `maxItems` | Cap on selected items (multi mode). `0` = unlimited. Once reached, additional clicks on unselected options are ignored. | `number` | `0` |
| `maxTagCount` | Maximum number of chips rendered before collapsing the rest into a `+N more` indicator. `0` = render every chip. | `number` | `0` |
| `value` | Unified value. Single mode holds a scalar (or `null`); multi / tag mode holds `readonly unknown[]`. Bound by `[formField]`, or two-way via `[(value)]`. | `unknown` | `null` |
| `(touch)` | Emitted on blur / commit so a bound field can mark itself touched. | `void` | — |

## Select outputs

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `(searchChange)` | Debounced query, for server-side search. Fires on every settled change — including `''` when cleared, so a store can reset. `[minChars]` does not gate it. | `OutputEmitterRef<string>` | `—` |
| `(searchQueryChange)` | Raw, undebounced query — the `[(searchQuery)]` half. Prefer `(searchChange)` for server calls. | `OutputRef<string>` | `—` |
| `(valueChange)` | The `[(value)]` half. Bound automatically by `[formField]` / `[(ngModel)]`. | `OutputRef<unknown>` | `—` |
| `(touch)` | Emitted on blur / commit so a bound field marks itself touched. | `OutputEmitterRef<void>` | `—` |

## Option API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `value`required | Form value contributed when chosen. | `unknown` | — |
| `disabled` | Disable this option. | `boolean` | `false` |

## Option Group API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `label`required | Section heading. | `string` | — |
