tngSelect
The root directive owns the committed single 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 selection semantics intact.
Root attachment
html
<section
tngSelect
[value]="selectedStage()"
(valueChange)="onSelectedStageChange($event)"
>
<button type="button" tngSelectTrigger>
<span tngSelectValue>{{ selectedStageLabel() ?? 'Choose workflow stage' }}</span>
<span tngSelectIcon aria-hidden="true">▾</span>
</button>
<div tngSelectContent>
<div tngSelectOverlay>
<div
tngSelectListbox
[value]="selectedStage()"
(valueChange)="onSelectedStageChange($event)"
>
@for (stage of workflowStages; track stage.value) {
<div tngSelectOption [tngValue]="stage.value">{{ stage.label }}</div>
}
</div>
</div>
</div>
</section>| Entry | Type | Details |
|---|---|---|
tngSelect | root directive | Owns open state, committed single value, disabled/loading/invalid flags, and ids used by the trigger + content pair. |
value / valueChange | string | null | Represents the committed option value. Headless usage usually normalizes the primitive payload back to a single string. |
open / openChange | boolean | Controls whether the menu is visible. Pointer and keyboard activation update this automatically unless you override it. |
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 text or your richer owned markup inside the trigger. |
tngSelectIcon | display slot | Optional icon slot. Use it 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. |
tngSelectListbox + tngSelectOption | listbox bridge | Connects the select root to listbox active/selected state and exposes option data attributes for styling. |
Listbox bridge
The select root is single-select, but the listbox bridge still uses the listbox value contract. Normalizing the primitive payload back to a single string keeps the owned headless model predictable.
Single-value normalization
ts
type SelectboxValue = string | readonly string[] | null;
readonly selectedStage = signal<string | null>('review');
onSelectedStageChange(value: SelectboxValue): void {
this.selectedStage.set(this.toSingleValue(value));
}
private toSingleValue(value: SelectboxValue): string | null {
if (typeof value === 'string') {
return value;
}
if (Array.isArray(value)) {
const first = value[0];
return typeof first === 'string' ? first : null;
}
return null;
}| Binding | Meaning | Why it matters |
|---|---|---|
[value] on tngSelect | committed model | The root stores the committed single value that should show in the trigger when the overlay closes. |
[value] on tngSelectListbox | selection mirror | Keeps the active/selected option in sync with the root when the listbox renders or the root changes. |
(valueChange) on both | safe headless pattern | Binding both ends keeps pointer, keyboard, and programmatic updates aligned with the committed single value. |
Reflected attributes
The primitive reflects stable slots and state so you can style the selectbox without reaching for brittle DOM assumptions.
Reflected state and slots
html
<section tngSelect data-slot="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">QA ready</span>
<span tngSelectIcon data-slot="select-icon">▾</span>
</button>
<div tngSelectContent data-slot="select-content">
<div tngSelectOverlay>
<div tngSelectListbox data-slot="select-listbox">
<div tngSelectOption data-slot="select-option" data-active data-selected>
QA ready
</div>
</div>
</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. |