Groups, and a filter
An item can be a group — a label with options under it — and filterable turns the trigger into an input that narrows the list as you type, the filter matching labels.
Deploying to: nowhere yet
A choice list with a panel of its own, holding one answer or many.
@pacit/components/selectpct-select, pct-multi-selectImplements the W3C ARIA APG Combobox pattern ↗
Shipping to: pl
import { Component, signal } from '@angular/core';
import { PctSelect, PctSelectOption } from '@pacit/components/select';
/** A listbox with a keyboard-first life: type to jump, arrows to walk, Enter to take. */
@Component({
selector: 'demo-select',
imports: [PctSelect],
styles: ':host { display: block; max-inline-size: 24rem; }',
template: `
<pct-select ariaLabel="Country" [options]="countries" [(value)]="country" />
<p>Shipping to: {{ country() ?? 'nowhere yet' }}</p>
`,
})
export class SelectDemo {
readonly countries: readonly PctSelectOption[] = [
{ value: 'pl', label: 'Poland' },
{ value: 'de', label: 'Germany' },
{ value: 'cz', label: 'Czechia', disabled: true },
{ value: 'ua', label: 'Ukraine' },
];
readonly country = signal<string | null>('pl');
}select.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctSelect, PctMultiSelect } from '@pacit/components/select';| Exported | What it is |
|---|---|
PctMultiSelectcomponentpct-multi-select | A many-choice select — the same panel, the same walk and the same file as pct-select, with a value that is a list. |
PctSelectcomponentpct-select | A single-choice select with a panel of its own (not a native <select>). |
PctSelectOptionContextinterface | What the select hands an option template on every row it draws. |
PctSelectOptionTemplatedirectiveng-template[pctSelectOption] | The template a consumer writes instead of the select's own option row (req-api-templates). |
PctSelectOptioninterface | An option of a select. The value is of any type T — business forms bind numeric ids, union members and whole entities, and narrowing it to a string forced every application into mapping by hand in both directions. T is a string by default, so string lists are written exactly as before. |
PctSelectPanelWidthtype'field' | 'auto' | (string & {}) | Width of the dropdown panel: - 'field' (the default) — exactly that of the visible control: the field border inside the chrome, the trigger alone outside it. The panel is then an extension of the field rather than a separate object, - 'auto' — as wide as the longest option, but never narrower than the control. For lists where an option's full text matters more than a flush edge, - a CSS length ('320px', '24rem') — the width outright. |
PctSelectPanelAligntype'start' | 'center' | 'end' | Alignment of the panel to the control when the panel does not have the control's width ('auto' or an explicit width). With panelWidth="field" every variant gives the same result. |
PctSelectOptionGroupinterface | A named section of a list — what a native <optgroup> is, and what the ARIA listbox pattern draws as a role="group" with its label referenced by aria-labelledby. |
PctSelectItemtypePctSelectOption<T> | PctSelectOptionGroup<T> | What options accepts: a flat list, a list of groups, or the two mixed — options standing before the first heading are exactly what a native <select> draws above its first <optgroup>. |
PctSelectFiltertype( option: PctSelectOption<T>, query: string, ) => boolean | What decides whether an option stays in the panel while a filter is on: the option, and what the user has typed. A predicate rather than a mode name ('contains', 'startsWith') — the moment a list wants matching on a second field, on a code beside the label or on what a server said, a name in an enum has to grow another member and a function does not. |
pctFilterByLabelconst | The default filter: the label, case-folded, contains what was typed. |
pctKeepAllconst | The predicate that decides nothing: for a list somebody else has already narrowed. An application filtering on a server answers the question with another options list, and the control would otherwise narrow that answer a second time — a server that matched a city by its old name, by a code or by a misspelling would watch the row it found be taken out again by a client-side includes, in a library that never said it would (0037). |
<pct-select ariaLabel="Country" [options]="countries" [(value)]="country" />Each example is the component itself, not a screenshot. Open the code under any of them.
An item can be a group — a label with options under it — and filterable turns the trigger into an input that narrows the list as you type, the filter matching labels.
Deploying to: nowhere yet
pct-multi-select holds an array: each pick toggles a value, the trigger names how many are chosen, and the same keyboard walks the same panel.
Via: email
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
unreleased marks 2 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.
PctSelectcomponent · pct-selectA single-choice select with a panel of its own (not a native <select>).
| Name | Type | What it does |
|---|---|---|
disableddefault false | boolean | Blocks the control and greys it — the native disabled, so it leaves the tab order as well. With [formField] the directive writes it, as it writes every input of the FormUiControl contract. |
readonlydefault false | boolean | Holds the value and keeps the panel closed while the control stays focusable; aria-readonly says so. |
invaliddefault false | boolean | The form's verdict; shown only once touched, so an empty form does not open red. |
toucheddefault false | boolean | Whether the user has left the field once; with invalid it gates the error face. |
requireddefault false | boolean | Marks the label with the required sign; with [formField] it follows the schema's required(). |
errorsdefault [] | readonly ValidationError.WithOptionalFieldTree[] | The form's validation errors; the first one's message takes the hint's place once the field is touched. |
warningsunreleaseddefault undefined | readonly ValidationError.WithOptionalFieldTree[] | undefined | A verdict without a veto (0087): shown after the error and before the hint once the field is touched, in the warning tone, with aria-invalid untouched. Left unbound, the control reads what pctWarn() wrote for its own [formField]; bound — [] included — the list given stands. |
namedefault '' | string | The native name — what a form submission calls the value. |
optionsdefault [] | readonly PctSelectItem<T>[] | The option list. The values have to be unique by compareWith: a value is what maps back to an option (the first match wins), so of two options sharing one value only the earlier is ever reachable — picking the later one shows the earlier one's label and leaves aria-selected on it. Which option an equal value denotes is not the component's to decide, so it does not repair the list quietly: in dev mode it says so. |
labeldefault '' | string | The visible label, rendered by the control itself when it stands outside a pct-field; inside one, the field's label is the name. |
hintdefault '' | string | A line of help under the control, outside a pct-field; the first error message takes its place while the field is invalid and touched. |
ariaLabeldefault '' | string | The accessible name of a control with no visible label — an INPUT rather than an aria-label written on the tag, because the tag cannot carry one: role="combobox" sits on the trigger inside, the host has no role at all, and an ARIA name on a roleless element is ignored (aria-label is prohibited for the generic role). Without this a standalone select is an unnamed combobox and the consumer has no way in (req-a11y-built-in). |
ariaLabelledbydefault '' | string | As ariaLabel, for a name that already stands somewhere on the page — a heading, the header of the column the control sits in. It wins over ariaLabel and over label, in ARIA's order and not ours. |
placeholder | string | The placeholder. With no value it comes from PCT_TEXTS — and it comes at render time, not at construction: an input's default value is produced once, so an application switching language at runtime would be left with the string from before the change (0014). placeholder="" stays an empty placeholder rather than a return to the default — absent and empty mean different things. |
sizedefault config.defaultSize | PctSize | Height 28 / 36 / 44 px — the axis every field shares; from providePctConfig by default (req-api-config). |
compareWithdefault pctSameValue | PctCompareWith<T> | Value equality. Identity by default, which for strings and numbers is the same as ===. Entities need comparison by key — an instance from the server is not the same reference as an option on the list, so without this the selected item would not highlight after the form loads. |
filterabledefault false | boolean | Whether the trigger is a text field and the list narrows to what is typed into it. |
filterTexttwo-waydefault '' | string | What has been typed into the trigger. A model, so an application filtering on a server can read the question and answer it with another options list — and the control still owns the clearing, because the question belongs to the panel: it is set to '' when the panel closes and when a pick answers it. It is not the value and never becomes one. |
filterWithdefault pctFilterByLabel | PctSelectFilter<T> | What counts as a match. The default folds case on the label and asks for includes; anything more than that — a second field, a code beside the label, an idea of which letters are the same letter — is the application's, because it is the application that knows the language (pctFilterByLabel, and lesson-101 for why). |
clearabledefault false | boolean | Whether the control draws a cross that takes back what the trigger is showing — the answer, or the question being typed while the panel is up (0036). |
loadingdefault false | boolean | The list is on its way. A fact about the list, not about the control (0037): it takes nothing away — not the focus, not the answer already given, not the rows still on the screen — and says two things instead. The panel carries aria-busy, so a reader knows that what it is reading may not be the last word, and an empty one says "loading" where it would otherwise have said "no options": that sentence is a conclusion, and a list still coming has reached none. |
virtualdefault false | boolean | Draw only the rows the panel can show. It is a promise about how many rows exist, and that is why it is the control's to make: the panel builds its own rows, so it is the one thing here that can decide not to (0033). |
panelWidthdefault 'field' | PctSelectPanelWidth | Width of the dropdown panel — equal to the control by default ('field'). The panel then comes out exactly from its edge, so the list reads as an extension of the field. 'auto' fits the width to the longest option (without narrowing the panel below the control), and a CSS length sets it outright. |
panelAligndefault 'start' | PctSelectPanelAlign | Alignment of the panel to the control when it is wider or narrower than it. |
valuetwo-waydefault null | NoInfer<T> | null | The selected value — a required field of the FormValueControl contract. The type is T | null, because "nothing selected" is a state reachable for every T: the select starts empty and the cross returns to it. |
emptyValuedefault null | NoInfer<T> | null | The value standing for no choice — what the cross (clearable) writes. null by default, but an application with a non-nullable field (plan: string) supplies its own (emptyValue=""), so that a clear does not write null into the model against its type. A form's reset writes nothing here: the value it resets to is the form's (0092). |
| Name | Payload | When it fires |
|---|---|---|
touch | void | Emitted on blur — lets the form mark the field as touched. |
The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-select | |
data-pct-size | size() | |
data-pct-open | open() ? "" : null | |
data-pct-invalid | showInvalid() ? "" : null | |
data-pct-warning | warningLit() ? "" : null | |
data-pct-disabled | disabled() ? "" : null | |
data-pct-multiple | multiple ? "" : null | The one state a consumer's stylesheet cannot derive from the parts: both controls draw the same DOM, so the tag is what tells them apart, and a tag is not a selector the parts contract exposes (req-api-attributes). |
data-pct-clearable | clearable() ? "" : null | Not "there is a cross standing" but "this control has one": the space it takes is reserved by the input alone, so a trigger's text does not reflow the moment an answer appears under it. |
data-pct-in-field | inField ? "" : null | Inside the chrome pct-field draws the border and the label — the control hands them over. |
PctMultiSelectcomponent · pct-multi-selectA many-choice select — the same panel, the same walk and the same file as pct-select, with a value that is a list.
| Name | Type | What it does |
|---|---|---|
disableddefault false | boolean | Blocks the control and greys it — the native disabled, so it leaves the tab order as well. With [formField] the directive writes it, as it writes every input of the FormUiControl contract. |
readonlydefault false | boolean | Holds the value and keeps the panel closed while the control stays focusable; aria-readonly says so. |
invaliddefault false | boolean | The form's verdict; shown only once touched, so an empty form does not open red. |
toucheddefault false | boolean | Whether the user has left the field once; with invalid it gates the error face. |
requireddefault false | boolean | Marks the label with the required sign; with [formField] it follows the schema's required(). |
errorsdefault [] | readonly ValidationError.WithOptionalFieldTree[] | The form's validation errors; the first one's message takes the hint's place once the field is touched. |
warningsunreleaseddefault undefined | readonly ValidationError.WithOptionalFieldTree[] | undefined | A verdict without a veto (0087): shown after the error and before the hint once the field is touched, in the warning tone, with aria-invalid untouched. Left unbound, the control reads what pctWarn() wrote for its own [formField]; bound — [] included — the list given stands. |
namedefault '' | string | The native name — what a form submission calls the value. |
optionsdefault [] | readonly PctSelectItem<T>[] | The option list. The values have to be unique by compareWith: a value is what maps back to an option (the first match wins), so of two options sharing one value only the earlier is ever reachable — picking the later one shows the earlier one's label and leaves aria-selected on it. Which option an equal value denotes is not the component's to decide, so it does not repair the list quietly: in dev mode it says so. |
labeldefault '' | string | The visible label, rendered by the control itself when it stands outside a pct-field; inside one, the field's label is the name. |
hintdefault '' | string | A line of help under the control, outside a pct-field; the first error message takes its place while the field is invalid and touched. |
ariaLabeldefault '' | string | The accessible name of a control with no visible label — an INPUT rather than an aria-label written on the tag, because the tag cannot carry one: role="combobox" sits on the trigger inside, the host has no role at all, and an ARIA name on a roleless element is ignored (aria-label is prohibited for the generic role). Without this a standalone select is an unnamed combobox and the consumer has no way in (req-a11y-built-in). |
ariaLabelledbydefault '' | string | As ariaLabel, for a name that already stands somewhere on the page — a heading, the header of the column the control sits in. It wins over ariaLabel and over label, in ARIA's order and not ours. |
placeholder | string | The placeholder. With no value it comes from PCT_TEXTS — and it comes at render time, not at construction: an input's default value is produced once, so an application switching language at runtime would be left with the string from before the change (0014). placeholder="" stays an empty placeholder rather than a return to the default — absent and empty mean different things. |
sizedefault config.defaultSize | PctSize | Height 28 / 36 / 44 px — the axis every field shares; from providePctConfig by default (req-api-config). |
compareWithdefault pctSameValue | PctCompareWith<T> | Value equality. Identity by default, which for strings and numbers is the same as ===. Entities need comparison by key — an instance from the server is not the same reference as an option on the list, so without this the selected item would not highlight after the form loads. |
filterabledefault false | boolean | Whether the trigger is a text field and the list narrows to what is typed into it. |
filterTexttwo-waydefault '' | string | What has been typed into the trigger. A model, so an application filtering on a server can read the question and answer it with another options list — and the control still owns the clearing, because the question belongs to the panel: it is set to '' when the panel closes and when a pick answers it. It is not the value and never becomes one. |
filterWithdefault pctFilterByLabel | PctSelectFilter<T> | What counts as a match. The default folds case on the label and asks for includes; anything more than that — a second field, a code beside the label, an idea of which letters are the same letter — is the application's, because it is the application that knows the language (pctFilterByLabel, and lesson-101 for why). |
clearabledefault false | boolean | Whether the control draws a cross that takes back what the trigger is showing — the answer, or the question being typed while the panel is up (0036). |
loadingdefault false | boolean | The list is on its way. A fact about the list, not about the control (0037): it takes nothing away — not the focus, not the answer already given, not the rows still on the screen — and says two things instead. The panel carries aria-busy, so a reader knows that what it is reading may not be the last word, and an empty one says "loading" where it would otherwise have said "no options": that sentence is a conclusion, and a list still coming has reached none. |
virtualdefault false | boolean | Draw only the rows the panel can show. It is a promise about how many rows exist, and that is why it is the control's to make: the panel builds its own rows, so it is the one thing here that can decide not to (0033). |
panelWidthdefault 'field' | PctSelectPanelWidth | Width of the dropdown panel — equal to the control by default ('field'). The panel then comes out exactly from its edge, so the list reads as an extension of the field. 'auto' fits the width to the longest option (without narrowing the panel below the control), and a CSS length sets it outright. |
panelAligndefault 'start' | PctSelectPanelAlign | Alignment of the panel to the control when it is wider or narrower than it. |
valuetwo-waydefault [] | NoInfer<T>[] | The chosen values — the FormValueControl contract's one field, here a list. Empty is the empty list, so there is no emptyValue to declare: [] is reachable for every T and means the same thing in every application, which null did not. |
| Name | Payload | When it fires |
|---|---|---|
touch | void | Emitted on blur — lets the form mark the field as touched. |
The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-select | |
data-pct-size | size() | |
data-pct-open | open() ? "" : null | |
data-pct-invalid | showInvalid() ? "" : null | |
data-pct-warning | warningLit() ? "" : null | |
data-pct-disabled | disabled() ? "" : null | |
data-pct-multiple | multiple ? "" : null | The one state a consumer's stylesheet cannot derive from the parts: both controls draw the same DOM, so the tag is what tells them apart, and a tag is not a selector the parts contract exposes (req-api-attributes). |
data-pct-clearable | clearable() ? "" : null | Not "there is a cross standing" but "this control has one": the space it takes is reserved by the input alone, so a trigger's text does not reflow the moment an answer appears under it. |
data-pct-in-field | inField ? "" : null | Inside the chrome pct-field draws the border and the label — the control hands them over. |
Two ways in, both stable. No class names to depend on.
In a test, PctSelectHarness, PctMultiSelectHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.
| Part | Selector | What it is |
|---|---|---|
arrow | [data-pct-part="arrow"] | the chevron at the end |
clear | [data-pct-part="clear"] | the button that empties the value |
empty | [data-pct-part="empty"] | the sentence shown when the list has nothing to show — beside the list, not in it (0069) |
error | [data-pct-part="error"] | the message when invalid |
group | [data-pct-part="group"] | a group of options |
group-label | [data-pct-part="group-label"] | the heading of a group |
hint | [data-pct-part="hint"] | the hint under the control |
label | [data-pct-part="label"] | the label of the control |
list | [data-pct-part="list"] | the listbox inside the panel — the element that scrolls, and the one the trigger controls |
option | [data-pct-part="option"] | one option |
option-check | [data-pct-part="option-check"] | the mark on a chosen option |
panel | [data-pct-part="panel"] | the list box |
placeholder | [data-pct-part="placeholder"] | the text while nothing is chosen |
trigger | [data-pct-part="trigger"] | the combobox element the panel opens from |
value | [data-pct-part="value"] | the chosen value as text |
warning | [data-pct-part="warning"] | the message when the value is allowed and suspect — after the error, before the hint (0087) |
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
| Token | Styles |
|---|---|
--pct-select-arrow-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the arrow. |
--pct-select-bg{pct.surface}#ffffff · #0f172a | Background of the select. |
--pct-select-bg-disabled{pct.surface-disabled}#f1f5f9 · #1e293b | Background of the select while disabled. |
--pct-select-border{pct.border-strong}#64748b | Edge colour of the select. |
--pct-select-border-disabled{pct.border}#e2e8f0 · #334155 | Edge colour of the select while disabled. |
--pct-select-border-focus{pct.primary}#2563eb · #60a5fa | Edge colour of the select when focused. |
--pct-select-border-hover{pct.text-muted}#334155 · #e2e8f0 | Edge colour of the select on hover. |
--pct-select-border-invalid{pct.danger}#b91c1c · #f87171 | Edge colour of the select when invalid. |
--pct-select-border-warning{pct.warning}#b45309 · #fbbf24 | Edge colour of the select under a warning (0087). |
--pct-select-fg{pct.text}#0f172a · #ffffff | Text colour of the select. |
--pct-select-fg-disabled{pct.text-disabled}#64748b | Text colour of the select while disabled. |
--pct-select-fg-invalid{pct.danger}#b91c1c · #f87171 | Text colour of the select when invalid. |
--pct-select-fg-warning{pct.warning}#b45309 · #fbbf24 | Text colour of the warning line (0087). |
--pct-select-font-size{pct.font.size.md}14px | Font size of the select. |
--pct-select-group-label-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the group label. |
--pct-select-group-label-font-size{pct.font.size.sm}13px | Font size of the group label. |
--pct-select-group-label-font-weight{pct.font.weight.medium}500 | Font weight of the group label. |
--pct-select-group-label-padding-x{pct.space.4}12px | Horizontal padding of the group label. |
--pct-select-group-label-padding-y{pct.space.2}4px | Vertical padding of the group label. |
--pct-select-hint-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the hint. |
--pct-select-label-fg{pct.text}#0f172a · #ffffff | Text colour of the label. |
--pct-select-option-bg{pct.surface}#ffffff · #0f172a | Background of an option. |
--pct-select-option-bg-active{pct.surface-100}#f1f5f9 · #1e293b | Background of the option the keyboard is on. |
--pct-select-option-bg-selected{pct.primary}#2563eb · #60a5fa | Background of an option when selected. |
--pct-select-option-fg{pct.text}#0f172a · #ffffff | Text colour of an option. |
--pct-select-option-fg-active{pct.text}#0f172a · #ffffff | Text colour of the option the keyboard is on. |
--pct-select-option-fg-disabled{pct.text-disabled}#64748b | Text colour of an option while disabled. |
--pct-select-option-fg-selected{pct.on-primary}#ffffff · #0f172a | Text colour of an option when selected. |
--pct-select-option-padding-x{pct.space.4}12px | Horizontal padding of an option. |
--pct-select-option-padding-y{pct.space.3}8px | Vertical padding of an option. |
--pct-select-padding-x{pct.space.4}12px | Horizontal padding of the select. |
--pct-select-padding-y{pct.space.3}8px | Vertical padding of the select. |
--pct-select-panel-bg{pct.surface}#ffffff · #0f172a | Background of the panel. |
--pct-select-panel-border{pct.border-strong}#64748b | Edge colour of the panel. |
--pct-select-panel-max-height240px | Maximum height of the panel. |
--pct-select-panel-radius{pct.radius.md}8px | Corner radius of the panel. |
--pct-select-panel-shadow{pct.shadow-panel}0 8px 24px rgba(15, 23, 42, 0.14) · 0 8px 24px rgba(0, 0, 0, 0.55) | Shadow of the panel. |
--pct-select-placeholder-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the placeholder. |
--pct-select-radius{pct.radius.md}8px | Corner radius of the select. |
A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.
[data-theme='brand'] {
--pct-select-border-focus: #0f766e;
--pct-select-option-bg-selected: #0f766e;
--pct-select-option-fg-selected: #ffffff;
}Shipping to: pl
| key | effect | test |
|---|---|---|
↑ / ↓ | open the panel / change the active option | apps/sandbox-e2e/src/select.spec.ts |
Home / End | first / last option | apps/sandbox-e2e/src/select.spec.ts |
Enter | pick the active option, close | apps/sandbox-e2e/src/select.spec.ts |
Enter (many) | toggle the active option, stay open | apps/sandbox-e2e/src/select.spec.ts |
Escape | close with no change | apps/sandbox-e2e/src/select.spec.ts |
Escape (shut, clearable) | take the answer back — the keyboard's cross | apps/sandbox-e2e/src/select.spec.ts |
| characters | typeahead, skipping disabled options | libs/components/select/src/select.spec.ts |
With filterable the map splits with the element the trigger is — the list keeps what it owns, the caret takes back what a text field answers on its own (req-api-platform):
| key | on a filtering trigger | test |
|---|---|---|
↑ / ↓ | open the panel / change the active option | apps/sandbox-e2e/src/select.spec.ts |
| characters | narrow the list — and open the panel doing it | apps/sandbox-e2e/src/select.spec.ts |
Home / End | the caret's: the ends of the text | apps/sandbox-e2e/src/select.spec.ts |
| space | a character, not a pick | apps/sandbox-e2e/src/select.spec.ts |
Enter | pick the active option (closed: the form's) | apps/sandbox-e2e/src/select.spec.ts |
Escape / Tab | close, and the question goes with the panel | libs/components/select/src/select.spec.ts |
| a click on the trigger | opens, and never closes — a caret is not a switch | libs/components/select/src/select.spec.ts |
The map is unchanged by a custom option row — the keys sit on the trigger, not on the option (apps/sandbox-e2e/src/select.spec.ts, "the keyboard is unchanged by a custom row").
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
apps/sandbox-e2e/src/select.spec.ts, libs/components/select/src/select.spec.tsapps/sandbox-e2e/src/a11y.spec.ts — including the panel with a scoped theme, the panel waiting for its list and the empty panel, whose sentence stands beside the listbox (0069)apps/sandbox-e2e/src/visual.spec.ts — separate select-panel-open and select-panel-loading shotsforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts — selection is carried by the background (SelectedItem) and the keyboard cursor by the outline (Highlight), so an option that is both selected and active shows both states at once. tools/check-styles.mjs (point 7) adds what no browser here shows: the active option's surface goes back to Canvas (the cursor is the outline), and inside the chrome the ring belongs to the field's row in this mode as well (lesson-70)prefers-reduced-motionapps/sandbox-e2e/src/preferences.spec.tsapps/sandbox-e2e/src/field-hitarea.spec.ts — this is where the 19.6 px regression surfaced (lesson-25)apps/sandbox-e2e/src/size.spec.ts — inside a wrapper the size is handed to the field; the panel takes its font size from the triggerapps/sandbox-e2e/src/rtl.spec.ts — the panel's COMPUTED direction compared with the trigger's, with the panel proved to sit outside app-root so the assertion is not ordinary inheritance (lesson-35); plus five -rtl baselines, four of them of the open panel, in apps/sandbox-e2e/src/visual.spec.ts, and tools/check-styles.mjs (point 5, req-token-logical) over the stylesheetapps/sandbox-e2e/src/hydration.spec.tslibs/components/select/src/select.spec.ts, apps/sandbox-e2e/src/forms.spec.ts; a form's reset — reset() keeping the value, reset(value) showing it, the form pristine: libs/components/select/src/select.spec.ts › signal forms and multi-select.spec.tslibs/components/select/src/select.spec.ts, apps/sandbox-e2e/src/select.spec.ts — the row rendered in three engines, with the listbox pattern and the keyboard intactPCT_ICONStools/check-icons.mjs (target check-icons), libs/components/select/src/select.spec.ts, apps/sandbox-e2e/src/select.spec.ts — the registered drawing in three engines, in the same box and turning with it on openingtools/check-aria.mjs point 9 — the option's owner read off the template's tree, with axe's requiredContext as the rule, before any page renders the panel (lesson-65); libs/components/select/src/select.spec.ts, apps/sandbox-e2e/src/select.spec.ts — the group named by the heading it draws and owning its options, in three engines; the walk crossing the headings; a disabled group out of reach. apps/sandbox-e2e/src/a11y.spec.ts — the open grouped panel audited, which is what says an option is still owned through a role="group". apps/sandbox-e2e/src/visual.spec.ts — a select-panel-groups baseline, a heading being the one thing here that is drawn and never touchedlibs/components/select/src/multi-select.spec.ts — the list-valued contract: the panel that stays open, the value in list order, a value the list cannot name, the marks, the walk opening on the first chosen row, and the input parity of the two tags read off ɵcmp.inputs. apps/sandbox-e2e/src/select.spec.ts — the two comboboxes side by side in three engines, where aria-multiselectable is measured as a difference rather than as an attribute somebody wrote. apps/sandbox-e2e/src/a11y.spec.ts — the open many-choice panel audited, which is what says every option carries aria-selected, the false half included. apps/sandbox-e2e/src/visual.spec.ts — select-panel-multiple and its RTL twin, the mark being drawn and never touchedlibs/components/select/src/select.spec.ts and multi-select.spec.ts — 26 cases: the trigger that is a text field, the list narrowing under the letters, the cursor on the first row still standing, the chosen label surviving a question that hides it, the caret's keys, the query that dies with the panel, and a many-choice pick that keeps what the question hid. apps/sandbox-e2e/src/select.spec.ts — 7 cases × 3 engines, where the letters really reach a field. apps/sandbox-e2e/src/a11y.spec.ts — the narrowed panel audited whole-page, because aria-controls and aria-activedescendant cross from the page into the overlay and a reference to an id that is not in the document is exactly what axe reports. apps/sandbox-e2e/src/visual.spec.ts — select-filter-trigger and its RTL twin: an <input> wearing the button's border with the arrow over its end padding, which no behavioural case toucheslibs/components/select/src/select.spec.ts and multi-select.spec.ts — 22 cases: what the cross takes back on each of the four states a trigger can be in, the value a list cannot name, the control nobody may change, Escape spent and Escape left alone, the press that keeps focus on the trigger, and the press that is not a click outside the panel. apps/sandbox-e2e/src/select.spec.ts — 8 cases × 3 engines, where the cross really is a sibling in a page that was parsed rather than built (lesson-104), Tab really walks past it, and the target really measures 24 px. apps/sandbox-e2e/src/a11y.spec.ts — the cross audited where it stands, its name coming from PCT_TEXTS. apps/sandbox-e2e/src/visual.spec.ts — select-clear-trigger and its RTL twin: two ways of ending a trigger that have to come out as one pair of iconslibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts)contrast.policy.jsonlibs/tokens/src/contrast.policy.jsonPCT_TEXTStools/check-texts.mjs + select.spec.ts — a partial override leaves the rest at the defaults, and a runtime language change reaches the stringslibs/components/size.snapshot.md — the ./select row, and in the second block what one of its two tags costs alonedocs/acr/at/ — three readers, whole sandbox, and the act that opens the list. ON OPEN, two of the three announce the listbox, and differently: Orca leaving main content. · List with 6 items · Poland · not selected. — the list, its size, the row the cursor points at and that row's state — and NVDA list, nothing more; VoiceOver was silent at the opening — focus stays on the trigger by design, so whether the list opened under it the record cannot say — and its one reading of a trigger here is , Sélectionn Country A list with a panel of its own (CDK Overlay) list box pop up collapsed combo box — a truncated value read before the label, as on 2026-09-16, and worth a look. On Escape Orca says collapsed. ON ARRIVING at a trigger the earlier reading stands: label, combobox role and that it opens a list from all three, the VALUE from two — NVDA Country, combo box, Poland, collapsed, opens list, Orca field (the default) · combo box. · opens listbox with no word of the value. A value change is a second act, not this one/components/select on the published site — prerendered, the demo's own source is the code tabRead at build time from the tracked snapshots, not typed here.
Not a native <select>, because the native one gives no panel whose look and content can be controlled — a deliberate exception to req-api-platform.
NoInfer<button> inside a <button> is two buttons, and only the parser says soaria-busy is the specification's way of saying "not yet"offsetHeight is an integer, and a list is arithmetic over a fractionpctSelectOption template drawing two lines moves every row below it. Reported in dev mode and not repaired — which of two heights is the right one is the application's question. A window and panelWidth="auto" are a poor pair for the same reason: "as wide as the longest option" is measured from the rows that exist, so the panel would change width as the user scrolls.pointer-events: none and is a sign rather than a button. Escape, Tab, a pick and an outside click all close it.filterText and their own template; the second belongs to whoever calls a server, because a predicate over an array in memory needs none.PCT_TEXTS that an application could not translate by handing over a word.<pct-option> is not coming. Measured rather than declined: <pct-option [value]="1"> beside [value]="'pl'" compiles silently, the parent's own query hands back any, and a thousand projected options are a thousand component instances on a page whose panel has never opened (0033, lesson-97, lesson-98).lesson-66). What is deliberately not measured: that the message ever reaches a browser console — no page renders a list with a duplicate, and the unit cases carry the proof instead.virtual is opt-in rather than a length the library decides for itself: a list that quietly stopped being searchable at the four hundredth option would be a defect nobody could report against a promise nobody made.