Select
A single- or multiple-choice dropdown built on the accessible @angular/aria combobox/listbox primitives, with a fully custom trigger and option rendering when the default chrome doesn't fit.
A dropdown that behaves like a native <select> — keyboard navigable, aria-selected, popup positioned near
the field — but accepts option and trigger content of any shape. SelectMenu (SelectComponent) covers both
single and multiple selection from one component.
Concept — FormValueControl
Angular Signal Forms recognizes a component as a form control when it exposes a value model matching the
FormValueControl<TValue> contract. SelectMenu implements it through its own value model, so [formField]
works exactly like it would on a native input — no adapter needed.
Minimal example
import { Component, signal } from "@angular/core";
import { SelectComponent } from "@sinequa/galactik";
@Component({
selector: "fruit-select",
imports: [SelectComponent],
template: `
<SelectMenu
[options]="fruits"
[(value)]="fruit"
fieldLabel="Fruit"
placeholder="Choose a fruit…"
hint="Pick your favorite." />
`,
})
export class FruitSelectComponent {
fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"];
fruit = signal<string | null>(null);
}How it works
The trigger and popup are the accessible @angular/aria combobox/listbox primitives — every role and ARIA
state (role="combobox", aria-expanded, aria-haspopup="listbox" on the trigger; role="listbox",
aria-activedescendant, aria-selected on the popup) comes from there, nothing to wire by hand. The popup is
positioned with @floating-ui/dom (offset(4), flip(), shift({ padding: 8 })), its width bound to the
trigger's, and reopening it scrolls the current selection into view automatically.
The row under the mouse becomes the active row (pointermove on the ngListbox, via activateOnHover), so only one
row is highlighted at a time and the arrow keys carry on from it; a still pointer under a scrolling list changes nothing.
Single vs. multiple only changes the shape of value and whether the popup closes on pick: single mode
(default) is TOption | null and closes the popup once a choice is made; multiple makes value an array,
keeps the popup open across picks, and shows a check mark via the option template's selected context.
Recipes
Multiple selection
import { Component, signal } from "@angular/core";
import { SelectComponent } from "@sinequa/galactik";
@Component({
selector: "fruits-multi-select",
imports: [SelectComponent],
template: `
<SelectMenu
multiple
[options]="fruits"
[(value)]="selected"
fieldLabel="Fruits"
placeholder="Select fruits…"
hint="Pick one or more — the popup stays open.">
<ng-template #selectOption let-fruit let-selected="selected">
<span class="select-option-body">
<span class="select-option-label">{{ fruit }}</span>
</span>
@if (selected) { <span class="select-option-check">✓</span> }
</ng-template>
</SelectMenu>
`,
})
export class FruitsMultiSelectComponent {
fruits = ["Apple", "Banana", "Cherry", "Date", "Elderberry"];
selected = signal<string[]>([]);
}Object options — custom key, label and row
key/label default to identity/String(option), correct only for primitive options. Any object-shaped
option needs explicit functions — otherwise the trigger prints [object Object] and selection comparisons
break (see Pitfalls).
import { Component, signal } from "@angular/core";
import { SelectComponent } from "@sinequa/galactik";
interface Country {
id: string;
label: string;
capital: string;
}
const COUNTRIES: Country[] = [
{ id: "fr", label: "France", capital: "Paris" },
{ id: "de", label: "Germany", capital: "Berlin" },
{ id: "es", label: "Spain", capital: "Madrid" },
];
@Component({
selector: "country-select",
imports: [SelectComponent],
template: `
<SelectMenu
[options]="countries"
[(value)]="selected"
[key]="keyOf"
[label]="labelOf"
fieldLabel="Country"
placeholder="Select a country…">
<ng-template #selectOption let-country let-selected="selected">
<span class="select-option-body">
<span class="select-option-label">{{ country.label }}</span>
<span class="select-option-sublabel">{{ country.capital }}</span>
</span>
@if (selected) { <span class="select-option-check">✓</span> }
</ng-template>
</SelectMenu>
`,
})
export class CountrySelectComponent {
countries = COUNTRIES;
selected = signal<Country | null>(null);
protected readonly keyOf = (c: Country) => c.id;
protected readonly labelOf = (c: Country) => c.label;
}A fully custom trigger
Project a [selectTrigger] element to replace the default trigger entirely — useful for a compact, chrome-less
trigger such as a sort selector. placeholder and #selectValue no longer apply once a custom trigger is
projected; render the current value yourself.
import { Component, signal } from "@angular/core";
import { SelectComponent, SelectTriggerDirective } from "@sinequa/galactik";
interface SortOption {
id: string;
label: string;
}
const SORT_OPTIONS: SortOption[] = [
{ id: "relevance", label: "Relevance" },
{ id: "date-desc", label: "Newest first" },
{ id: "date-asc", label: "Oldest first" },
];
@Component({
selector: "sort-select",
imports: [SelectComponent, SelectTriggerDirective],
template: `
<SelectMenu [options]="sortOptions" [(value)]="sort" [key]="keyOf" [label]="labelOf">
<button selectTrigger type="button" class="select-trigger">
Sort: {{ labelOf(sort()) }}
</button>
<ng-template #selectOption let-o let-selected="selected">
<span class="select-option-label">{{ o.label }}</span>
@if (selected) { <span class="select-option-check">✓</span> }
</ng-template>
</SelectMenu>
`,
})
export class SortSelectComponent {
sortOptions = SORT_OPTIONS;
sort = signal<SortOption>(SORT_OPTIONS[0]);
protected readonly keyOf = (o: SortOption) => o.id;
protected readonly labelOf = (o: SortOption) => o.label;
}Options
Prop
Type
Pitfalls
What's next
Search Bar
A self-contained, pill-shaped search field with a leading icon and built-in clear/send controls, meant to stand on its own rather than inside a form.
Autocomplete
An editable combobox for debounced backend search with multi-selection — labels, filter facets, people pickers — where picked values render as removable chips inside the field.