tngMultiSelect
The root directive owns the committed array value, open state, and the aria wiring that the trigger and content use. Headless usage means you control the DOM around it while the primitive keeps multi-selection semantics intact.
Root attachment
html
<section
tngMultiSelect
[value]="selectedPlanets()"
(valueChange)="onValueChange($event)"
>
<button type="button" tngSelectTrigger>
<span tngSelectValue>{{ selectedSummary() }}</span>
<span tngSelectIcon aria-hidden="true">▾</span>
</button>
<div tngSelectContent>
<div tngSelectOverlay>
<ul
tngMultiSelectListbox
[multiple]="true"
[value]="selectedPlanets()"
>
@for (planet of planets; track planet.value) {
<li tngMultiSelectOption [tngValue]="planet.value">{{ planet.label }}</li>
}
</ul>
</div>
</div>
</section>| Entry | Type | Details |
|---|---|---|
tngMultiSelect | root directive | Owns open state, committed array value, disabled/loading/invalid flags, and ids used by the trigger + content pair. |
value / valueChange | readonly T[] | Represents the committed set of selected option values. Emitted as an array after every toggle. |
open / openChange | boolean | Controls whether the menu is visible. Enter toggles the active option without closing; outside click or Escape closes. |
disabled, loading, invalid | boolean | Reflected onto the root host for semantics and styling, then consumed by the trigger and overlay parts. |
Owned parts
Each part directive is small on purpose. You own the markup; the primitive annotates it with slots, state, and interaction behavior.
| Part | Role | Guidance |
|---|---|---|
tngSelectTrigger | combobox trigger | Focus target and keyboard entry point. The trigger toggles the menu and owns aria-expanded + aria-controls. |
tngSelectValue | display slot | Receives the committed label summary or your richer owned markup inside the trigger. |
tngSelectIcon | display slot | Optional icon slot for chevrons or status glyphs without changing trigger semantics. |
tngSelectContent + tngSelectOverlay | overlay shell | The content wrapper tracks hidden state while the overlay carries the portaled menu surface. |
tngMultiSelectListbox + tngMultiSelectOption | listbox bridge | Connects the multiselect root to listbox active/selected state. Options expose data attributes for styling. |
Listbox bridge
The multiselect root models values as arrays. Normalize the emitted payload to readonly string[] in your handler.
Array-value normalization
ts
readonly selectedPlanets = signal<readonly string[]>(['earth', 'mars']);
onValueChange(value: unknown): void {
this.selectedPlanets.set(this.toValueArray(value));
}
private toValueArray(value: unknown): readonly string[] {
if (value === null || value === undefined) return [];
if (Array.isArray(value))
return value.filter((v): v is string => typeof v === 'string');
return typeof value === 'string' ? [value] : [];
}| Binding | Meaning | Why it matters |
|---|---|---|
[value] on tngMultiSelect | committed model | The root stores the committed array of values that should show in the trigger summary. |
[multiple]="true" on tngMultiSelectListbox | multi-selection mode | Enables multi-selection semantics so Enter toggles instead of closing. |
(valueChange) on root | safe headless pattern | Emits the full updated array after each toggle. Normalize to readonly string[] in your handler. |
Reflected attributes
The primitive reflects stable slots and state so you can style the multiselect without reaching for brittle DOM assumptions.
Reflected state and slots
html
<section tngMultiSelect data-slot="multi-select" data-state="open">
<button
tngSelectTrigger
role="combobox"
aria-expanded="true"
aria-controls="tng-select-content-..."
data-slot="select-trigger"
>
<span tngSelectValue data-slot="select-value">Earth, Mars</span>
<span tngSelectIcon data-slot="select-icon">▾</span>
</button>
<div tngSelectContent data-slot="select-content">
<div tngSelectOverlay>
<ul tngMultiSelectListbox data-slot="multi-select-listbox">
<li tngMultiSelectOption data-slot="multi-select-option" data-active data-selected>
Earth
</li>
</ul>
</div>
</div>
</section>| Attribute | Scope | Purpose |
|---|---|---|
data-slot | structural marker | Added to the root, trigger, value, icon, content, listbox, and option parts for styling hooks. |
data-state | root state | The root reflects open or closed state so your shell can respond when the menu is visible. |
data-active / data-selected / data-disabled | option state | Applied to option rows for hover-equivalent focus, committed selection, and disabled styling. |