CngxSelect
projects/forms/select/single-select/select.component.ts
Import#
import { CngxSelect } from '@cngx/forms/select'
Description#
Native-feeling single-select dropdown. Behaves like <select>,
exceeds mat-select on a11y. Stateless signal graph (ARIA, panel
view, option model, commit-controller surface) lives in
createSelectCore.
Metadata#
Host#
Providers#
CNGX_FORM_FIELD_CONTROL- useExisting
CngxSelect CNGX_STATEFUL- useFactory
(): { readonly state: CngxAsyncState} => { const self = inject(CngxSelect); return { state: self.commitState }; } CNGX_SELECT_PANEL_HOST- useExisting
CngxSelect CNGX_SELECT_PANEL_VIEW_HOST- useExisting
CngxSelect
Relationships
Used by4
Depends on11
Index#
Inputs
Derived State
HostBindings
Inputs#
TemplateRef | null Replaces the built-in ▾ caret glyph. Ignored when
*cngxSelectCaret is projected.
nullTemplateRef | null Replaces the built-in ✕ glyph inside the default clear button
while keeping the button frame, ARIA, and click handler. Ignored
when *cngxSelectClearButton is projected.
nullCngxSelectCommitAction | null nullCngxCommitErrorAnnouncePolicyScalar-commit error-announce policy. 'verbose' reads the error
message; 'soft' reads "selection removed". Per-instance input wins
over CngxSelectConfig.commitErrorAnnouncePolicy; default
{ kind: 'verbose', severity: 'assertive' }.
this.config.commitErrorAnnouncePolicy ?? { kind: 'verbose', severity: 'assertive' }, CngxSelectCompareFn cngxSelectDefaultCompare as CngxSelectCompareFn<T>, CngxSelectOptionsInput[] as CngxSelectOptionsInput<T>PopoverPlacementPopover placement relative to the trigger. Per-instance input
wins over CngxSelectConfig.popoverPlacement.
this.config.popoverPlacementnullT | undefinedOutputs#
unknownCngxSelectOptionDef | null CngxSelectChangeAsyncStatusT | undefinedInstance Properties#
unknownCurrently selected option, resolved against options. Structural
equal on .value under compareWith - fresh OptionDef references
for the same value don't cascade downstream re-renders (server
refetch pattern).
computed<CngxSelectOptionDef<T> | null>(() => this.selectedOption(), {
equal: (a, b) => {
if (a === b) {
return true;
}
if (a === null || b === null) {
return false;
}
return (this.compareWith() as CngxSelectCompareFn<unknown>)(a.value, b.value);
},
})Methods#
patchData(item: CngxSelectOptionDef<T>)Append a pre-built option to the local buffer. Renders in the next
panel emission and silently drops once the server includes a
matching value. Idempotent under compareWith.
HostBindings#
| Binding | Expression |
|---|---|
[id] | resolvedId() |
[attr.aria-readonly] | ariaReadonly() |
Structural styles shared across the entire CngxSelect family -
imported via styleUrls from every variant (CngxSelect,
CngxMultiSelect, CngxCombobox, CngxTypeahead, CngxTreeSelect,
CngxReorderableMultiSelect, CngxActionSelect,
CngxActionMultiSelect, CngxSelectShell, and the panel components).
All variants run encapsulation: None so the rules below are global;
the .cngx-select__* BEM-style class prefix is the namespace.
State modifiers
.cngx-option--highlightedon an option row - primary-tinted background marking the keyboard-focused row[aria-disabled='true']on an option - opacity 0.5 +cursor: not-allowed.cngx-select__option--pending-cursor: progresswhile a row's commit is in flight.cngx-select__chip-list--reordering- whole-stripcursor: grabbingwhile a drag is active.cngx-select__chip--draggingon a chip - Trello-style lift (shadow + scale + tilt + accent fill).cngx-select__chip--drag-overon a chip - vertical drop bar painted on the leading edge via::before
Variants
Chip-list overflow modes via data-overflow:
- default - flex-wrap chips across multiple rows
data-overflow='scroll-x'- single row + horizontal scrolldata-overflow='truncate'- single row + clipped overflow + the.cngx-select__chip-overflow-badgecount pill
Error surfaces with two layouts:
.cngx-select__error- column-stacked block (message + retry).cngx-select__error--inline- inline banner above the option list.cngx-select__commit-error- per-commit failure banner
Slots
.cngx-select__panel- the floating panel surface (anchor-positioned viaposition-try-fallbacks).cngx-select__group-header- uppercase optgroup label.cngx-select__option/.cngx-select__check- option row + the selected-state checkmark.cngx-select__loading/.cngx-select__empty- inline message rows.cngx-select__skeleton/.cngx-select__skeleton-row- first-load shimmer placeholders (static underprefers-reduced-motion).cngx-select__spinner-wrap/.cngx-select__spinner- first-load spinner ring.cngx-select__loading-bar- first-load top-edge progress bar.cngx-select__refreshing/.cngx-select__refreshing-spinner/.cngx-select__refreshing-dots- subsequent-load indicators.cngx-select__option-spinner/.cngx-select__option-error- per-row commit feedback.cngx-select__chip-list/.cngx-select__chip-overflow-badge- chip strip + overflow count.cngx-select__chip-wrap/.cngx-select__chip-handle- reorderable chip surface + optional drag-handle glyph.cngx-select__error/.cngx-select__error--inline/.cngx-select__error-message/.cngx-select__error-retry/.cngx-select__commit-error- error block, banner, and retry pieces
Inheritance
Tokens are registered top-level in @layer cngx.tokens via @property
so any consumer (cngx.theme layer, app layer) can override without
learning the type.
Panel surface, every primary-tinted glyph, and every danger-toned error surface delegate to the foundation so a dark-mode swap cascades through the floating panel even when anchor positioning detaches it from the trigger's DOM context:
--cngx-select-panel-border->--cngx-color-border--cngx-select-panel-bg->--cngx-color-surface--cngx-select-panel-color->--cngx-color-text--cngx-select-panel-shadow->--cngx-shadow-md--cngx-select-check-color->--cngx-color-primary--cngx-select-spinner-color->--cngx-color-primary--cngx-select-loading-bar-color->--cngx-color-primary--cngx-select-refreshing-color->--cngx-color-primary--cngx-select-option-spinner-color->--cngx-color-primary--cngx-select-option-error-color->--cngx-color-danger--cngx-select-error-color->--cngx-color-danger--cngx-select-chip-remove-hover-color->--cngx-color-danger--cngx-select-chip-drag-bg->--cngx-color-primary--cngx-select-chip-drop-bar-color->--cngx-color-primary--cngx-select-trigger-invalid-border-color->--cngx-color-danger--cngx-select-trigger-invalid-outline-color->--cngx-color-danger
--cngx-select-panel-min-width is intentionally NOT registered via
@property so the anchor-size(width) fallback survives when the
consumer doesn't override it; registering it with any initial-value
would shadow the var() fallback and break anchor positioning.
Dark mode
Three hooks swap alpha-on-black surfaces (panel shadow, option highlight, spinner track, chip-overflow tint, chip remove-hover tint, chip handle muted, chip drag shadow) plus the skeleton shimmer gradient and the option-spinner track:
prefers-color-scheme: dark[data-color-scheme="dark"].darkclass
Foundation-delegated tokens ride the foundation cascade unchanged.
Pair with
@cngx/themes/material/select-theme- Material 3 surface treatment across the entire family
Trigger skin for CngxSelect - the scalar single-select. The host
carries .cngx-select; this file owns only the trigger-button visual
and the host layout - the parts that differ per select-type. Panel
frame, option rows, skeletons, spinners, error banners, and
refreshing indicators ride on shared/select-base.css (loaded via
this component's styleUrls).
State modifiers
:focus-visibleon.cngx-select__trigger- focus-ring outline + offset[aria-disabled='true']on the trigger - opacity dim +cursor: not-allowed[aria-invalid='true']on the trigger - danger border + box-shadow glow at focus-within time:hoveron.cngx-select__clear- opacity 1:focus-visibleon the clear button - 2px primary outline
Slots
.cngx-select__root-display: contentshost shim.cngx-select__trigger- the borderedrole="combobox"row (user-select: noneprevents drag-click text selection).cngx-select__label- ellipsis-truncating value label (inline-flex with a small gap between an optional glyph and text).cngx-select__caret- panel-state glyph.cngx-select__clear- reset button
Inheritance
Trigger surface + focus tokens delegate to the foundation so dark-mode
swaps on --cngx-color-border / --cngx-color-primary cascade
through the trigger host:
--cngx-select-border->--cngx-color-border--cngx-select-focus-outline->--cngx-color-primary- invalid surfaces fall back to
--cngx-select-trigger-invalid-*->--cngx-color-danger
Shorthand tokens pin their structural part (1px solid / 2px solid) and inherit only the colour through the delegating chain.
Pair with
@cngx/themes/material/select-theme- Material 3 surface treatment shared across the entire select family
Index#
Surface
Layout
State / Highlighted
State / Selected
State / Disabled
State / Loading
State / Refreshing
State / Commit
State / Remove
State / Dragging
State / Error
State / Trigger invalid
Surface
*1px solid oklch(0.85 0.01 250)Border shorthand of the dropdown panel. Falls back through
--cngx-color-border. inherits: true so the :root delegating
value reaches the floating panel host (anchor-positioned, may
sit outside the normal DOM flow).
See: [[--cngx-color-border]]
<color>oklch(1 0 0)Background of the dropdown panel. Falls back through
--cngx-color-surface.
See: [[--cngx-color-surface]]
*currentColorText color inside the panel. syntax: '*' + initial-value
currentColor lets the panel inherit text color from its
ancestor by default. Registering as <color> would forbid
currentColor since <color> requires a computationally-
independent value.
*0 4px 12px oklch(0 0 0 / 0.12)Drop-shadow shorthand. Falls back through --cngx-shadow-md.
inherits: true so the :root dark-mode override (deeper shadow
on dark) reaches the floating panel host.
<color>oklch(0 0 0 / 0.5)Color of the placeholder text shown when no value is selected.
<color>oklch(0 0 0 / 0.5)Color of the dropdown caret glyph shared across every variant.
Muted by default to mirror the placeholder token; the Material
bridge maps it to --mat-sys-on-surface-variant. inherits: true
so a trigger-level override reaches the glyph element.
<color>oklch(0 0 0 / 0.5)Color of the clear-button glyph shared across every variant. Tracks
the same muted default as the caret so both trigger affordances read
uniformly. inherits: true so a trigger-level override reaches the
glyph element.
Layout
*16remMaximum height before vertical scrolling kicks in.
*autoMinimum height of an option row - defaults to auto so dense
rows stay compact.
*1.25emFont-size of the dropdown caret glyph shared across every variant.
*0.25remCorner radius of the reorderable chip wrap container.
*0.25remGap between the chip body and any projected drag handle.
State / Highlighted
<color>oklch(0.66 0.19 50 / 0.1)Background of the keyboard-highlighted option row. Defaults to a
low-alpha tint of --cngx-color-primary so the highlight reads
as brand-accented without competing with selection state.
See: [[--cngx-color-primary]]
State / Selected
<color>oklch(0.66 0.19 50)Color of the selected-option checkmark glyph. Falls back through
--cngx-color-primary.
See: [[--cngx-color-primary]]
State / Disabled
<number>0.5Opacity multiplier applied when the trigger is disabled. Shared family-wide so every variant dims its disabled trigger identically.
State / Loading
*0.125remCorner radius of each skeleton placeholder row.
*2px solid oklch(0 0 0 / 0.15)Track stroke of the first-load spinner ring. inherits: true
so the :root dark-mode override (alpha-on-white on dark)
reaches the spinner inside the floating panel.
<color>oklch(0.66 0.19 50)Indicator stroke of the first-load spinner ring. Falls back to
--cngx-color-primary.
<color>oklch(0.66 0.19 50)Color of the first-load loading bar. Falls back to
--cngx-color-primary.
State / Refreshing
<length>2pxHeight of the subsequent-load refreshing bar.
<color>oklch(0.66 0.19 50)Color of the refreshing bar gradient. Falls back to
--cngx-color-primary.
*0.25remPadding around the refreshing spinner wrapper.
*0.375remPadding around the refreshing dots block.
State / Commit
<color>oklch(0.66 0.19 50)Indicator stroke of the per-row commit spinner.
<color>oklch(0.6 0.18 25)Glyph color of the per-row commit error indicator. Falls back to
--cngx-color-danger.
*0.375rem 0.5remPadding of the commit error banner.
State / Overflow
<color>oklch(0 0 0 / 0.08)Background of the chip overflow badge shown in truncate overflow mode.
<color>oklch(0 0 0 / 0.6)Text color of the chip overflow badge.
State / Remove
*1.25remHit-target diameter of the chip remove button inside a reorderable chip wrap.
<color>oklch(0 0 0 / 0.12)Background tint of the chip remove button on hover.
<color>oklch(0.6 0.18 25)Foreground color of the chip remove button on hover. Falls back
to --cngx-color-danger.
State / Reorder
<color>oklch(0.5 0.01 250)Color of the optional projected drag-handle glyph.
*0.75remFont-size of the optional projected drag-handle glyph.
State / Dragging
*0 8px 20px oklch(0 0 0 / 0.28)Drop-shadow of the chip lifted into the dragging state.
inherits: true so the :root dark-mode override (deeper shadow
on dark) reaches the chip while it's being dragged.
<color>oklch(0.66 0.19 50)Background of the chip lifted into the dragging state. Falls back
to --cngx-color-primary.
See: [[--cngx-color-primary]]
<color>oklch(1 0 0)Text color of the chip lifted into the dragging state.
<number>1.06Scale multiplier of the dragging chip - Trello-style lift.
<angle>-1.5degRotation tilt applied to the dragging chip - Trello-style lift.
<length>3pxWidth of the drop-indicator bar between chips.
<color>oklch(0.66 0.19 50)Color of the drop-indicator bar between chips. Falls back to
--cngx-color-primary.
State / Error
<color>oklch(0.6 0.18 25)Text color of every error surface. Falls back to
--cngx-color-danger.
See: [[--cngx-color-danger]]
*0.375rem 0.5remPadding of the inline error banner shown above the option list.
*1px solid currentColorBorder shorthand of the error retry button.
State / Trigger invalid
<color>oklch(0.6 0.18 25)Border color painted on the trigger wrapper when aria-invalid="true".
Defaults track --cngx-color-danger via the delegating :root entry below.
<length>1pxBorder width painted on the trigger wrapper when aria-invalid="true".
<color>oklch(0.6 0.18 25)Outline color layered on the invalid trigger when focus is inside it.
Defaults track --cngx-color-danger via the delegating :root entry below.
*0 0 0 3px oklch(0.6 0.18 25 / 0.2)Soft halo layered behind the invalid trigger at focus time. Authored as a
box-shadow so it composes with any consumer-supplied focus outline.
State / Focus
*2px solid oklch(0.66 0.19 50)Focus-ring outline shorthand. Falls back to --cngx-color-primary.
See: [[--cngx-color-primary]]
Material theme
import { ChangeDetectionStrategy, Component, ViewEncapsulation, signal } from '@angular/core';
import { CngxSelect, type CngxSelectOptionDef } from '@cngx/forms/select';
/**
* `CngxSelect` rendered against a Material 3 palette via the published
* `@cngx/themes/material/select-theme` bridge.
*
* The stylesheet builds a real M3 theme: `mat.theme` emits the `--mat-sys-*`
* system tokens, then `select.theme($theme)` routes every `--cngx-select-*`
* token onto its Material counterpart, so the trigger, panel surface, and
* selected-option tones inherit the palette with no per-instance overrides.
* `ViewEncapsulation.None` lets the global `html` theme and the
* `:where(cngx-select)` bridge rules reach both the trigger and the
* top-layer panel.
*/
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
imports: [CngxSelect],
styleUrl: './material-theme.component.scss',
template: `
<div class="demo">
<cngx-select
[label]="'Favorite color'"
[options]="colors"
[(value)]="value"
[clearable]="true"
[selectionIndicatorPosition]="'after'"
placeholder="Pick a color…"
/>
<p class="demo__readout">Selected: {{ value() ?? '—' }}</p>
</div>
`,
})
export class MaterialThemeExample {
protected readonly value = signal<string | undefined>(undefined);
protected readonly colors: CngxSelectOptionDef<string>[] = [
{ value: 'red', label: 'Red' },
{ value: 'green', label: 'Green' },
{ value: 'blue', label: 'Blue' },
{ value: 'amber', label: 'Amber' },
{ value: 'violet', label: 'Violet' },
];
}
export { MaterialThemeExample as AppComponent };
Commit action
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Observable } from 'rxjs';
import {
CngxSelect,
CngxSelectCommitError,
type CngxSelectCommitAction,
type CngxSelectOptionDef,
} from '@cngx/forms/select';
/**
* CngxSelect commit-action — optimistic / pessimistic with rollback.
*
* `[commitAction]` runs an async save operation before the selection
* actually commits. In `optimistic` mode the option is selected
* immediately, the panel closes, and an error rolls the value back
* (with the previously selected option restored). In `pessimistic`
* mode the panel stays open and a per-row spinner shows on the intended
* option until the action resolves; the panel only closes on success.
*
* Toggle "Simulate error" and the mode buttons to exercise all four
* quadrants. The `*cngxSelectCommitError` slot template controls the
* inline error UI inside the panel — by default a banner above the
* options carries `error.message`.
*/
@Component({
selector: 'app-root',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CngxSelect, CngxSelectCommitError],
template: `
<p style="margin: 0 0 12px; opacity: 0.8; font-size: 0.875rem">
Pick a status — the commit-action simulates a server save. Tick
"Simulate error" and watch the rollback (optimistic) or the panel
staying open with the per-row spinner (pessimistic).
</p>
<div
style="display: flex; gap: 12px; align-items: center; margin-bottom: 12px; flex-wrap: wrap"
>
<label>
<input
type="checkbox"
[checked]="shouldFail()"
(change)="shouldFail.set($any($event.target).checked)"
/>
Simulate error
</label>
<button
type="button"
(click)="mode.set('optimistic')"
[style.fontWeight]="mode() === 'optimistic' ? 'bold' : 'normal'"
>
optimistic
</button>
<button
type="button"
(click)="mode.set('pessimistic')"
[style.fontWeight]="mode() === 'pessimistic' ? 'bold' : 'normal'"
>
pessimistic
</button>
</div>
<cngx-select
label="Status"
placeholder="Pick a status…"
[options]="options"
[(value)]="value"
[commitAction]="commitAction"
[commitMode]="mode()"
>
<ng-template cngxSelectCommitError let-error>
{{ error?.message }}
</ng-template>
</cngx-select>
<p style="margin-top: 12px">Selected: {{ value() ?? '–' }}</p>
`,
})
export class CommitActionExample {
protected readonly options: CngxSelectOptionDef<string>[] = [
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'In Review' },
{ value: 'approved', label: 'Approved' },
{ value: 'published', label: 'Published' },
];
protected readonly value = signal<string | undefined>(undefined);
protected readonly mode = signal<'optimistic' | 'pessimistic'>('optimistic');
protected readonly shouldFail = signal(false);
protected readonly commitAction: CngxSelectCommitAction<string> = (intended) =>
new Observable<string | undefined>((sub) => {
const handle = setTimeout(() => {
if (this.shouldFail()) {
sub.error(new Error('Server rejected: ' + intended));
} else {
sub.next(intended);
sub.complete();
}
}, 900);
return () => clearTimeout(handle);
});
}
export { CommitActionExample as AppComponent };