GUILD OF GLEKS UIv21.14.0

gog-multiselect

Multiselect

A checkbox-driven multiselect dropdown with optional select-all/clear controls, five sizes, disabled options, placement control, and a body-portaled panel. Once open, Arrow Up/Down move between options, Home/End jump to the first/last, and Escape closes.

Overview

Import the component and drop it into a template.

typescript
import { MultiselectComponent } from '@guildofgleks/ui';

@Component({
  // ...
  imports: [MultiselectComponent],
})

Basic usage — a labeled field bound with [(value)]:

<gog-multiselect label="Features" [options]="features" [(value)]="selectedFeatures" />

Examples

Selected names

The component also exposes a public selectedNames signal — the joined display names of the current selection — reachable straight off a template reference variable, with no summary computed by hand.

Selected: Bug

<gog-multiselect #ms label="Tags" [options]="tags" [(value)]="fullWidthTags" />
<p>Selected: {{ ms.selectedNames() }}</p>

Sizes

Five size steps, from xsm to slg.

@for (sizeOption of sizes; track sizeOption) {
  <gog-multiselect [label]="'Size: ' + sizeOption" [size]="sizeOption" [options]="features" [(value)]="sizeDemoValue" />
}

"Select all" / "Clear" controls

showControls adds a select-all/clear row; controlsPosition puts it above or below the option list — either way it's pinned with position: sticky, so it stays visible while a long list scrolls.

<gog-multiselect
  label="Top (default)"
  [options]="countries"
  [showControls]="true"
  [(value)]="topControlsValue"
/>

<gog-multiselect
  label="Bottom"
  [options]="countries"
  [showControls]="true"
  controlsPosition="bottom"
  [(value)]="bottomControlsValue"
/>

Disabled option & disabled field

A single option can be marked disabled without disabling the whole field, or the trigger itself can be disabled with [disabled]="true". The last field shows an error computed entirely by the page — gog-multiselect just renders whatever errorMessage it's given, for as long as it's non-empty.

Pick at least one option.
<gog-multiselect label="Disabled" [options]="features" [value]="['toast']" [disabled]="true" />

<gog-multiselect label="Permissions (one disabled)" [options]="permissionsWithDisabled" [(value)]="permissions" />

<gog-multiselect
  label="Required tags"
  placeholder="Pick at least one..."
  [options]="permissionsWithDisabled"
  [errorMessage]="requiredError()"
  [(value)]="requiredValue"
/>

Reactive Forms — automatic error timing

Same idea as gog-select: errorDisplay="auto" shows the error once the attached [formControl] has been touched and is invalid, with no manual error computation on this page's side.

<gog-multiselect
  label="Permissions"
  placeholder="Pick at least one..."
  [options]="permissionsWithDisabled"
  [formControl]="permissionsFormControl"
  errorMessage="Pick at least one permission."
  errorDisplay="auto"
/>

Full width

Full width of its container by default (see every field above). [fullWidth]="false" shrinks the trigger to fit its selected summary instead. The dashed outline is the row both share.

<gog-multiselect label="Features" [options]="features" [(value)]="fullWidthFeatures" />
<gog-multiselect label="Tags" [options]="tags" [(value)]="fullWidthTags" [fullWidth]="false" />

Custom chevron & label-less field

Project an <ng-template gogDropdownChevron> to swap the trigger's icon, and <ng-template gogMultiselectClearIcon> to swap the clear control's. ariaLabel names a field that has no visible label at all.

<gog-multiselect [options]="sortOptions" [(value)]="sortValue">
  <ng-template gogDropdownChevron>
    <gog-icon name="sort" />
  </ng-template>
  <ng-template gogMultiselectClearIcon>
    <gog-icon name="error" />
  </ng-template>
</gog-multiselect>

<gog-multiselect
  ariaLabel="Tags (no visible label)"
  placeholder="Pick tags"
  [options]="tags"
  [(value)]="ariaOnlyValue"
/>

Append to body & custom panel size

appendToBody portals the panel into document.body — useful inside scrollable or overflow-clipped containers, since the panel escapes the clipping. dropdownWidth and dropdownMaxHeight then take any CSS length to override the trigger-derived size; both apply only with appendToBody. The list below has 20 options, capped to 160px so it scrolls internally.

<gog-multiselect
  label="Country (fixed 240px / 160px panel)"
  [options]="countries"
  [appendToBody]="true"
  dropdownWidth="240px"
  dropdownMaxHeight="160px"
  [(value)]="compactPanelValue"
/>

Your own objects

The same accessors Select takes: optionLabel, optionValue and optionDisabled each accept a property path — dot-paths included — or a function. With [optionValue]="null" the control emits the option objects themselves. Defaults are 'name' / 'id' / 'disabled', so pre-21.3.0 code is unaffected.

value = (empty)

<gog-multiselect
  label="Reviewers"
  optionLabel="profile.fullName"
  optionValue="uuid"
  optionDisabled="suspended"
  [options]="users"
  [(value)]="reviewerIds"
/>

Filtering

filter puts a search box in the panel. The important detail is what it does to "select all": that control takes only the visible options, so it means what it says while a filter is active rather than quietly selecting the whole list. The query resets when the panel closes.

The search box has square corners, and that is the concentric answer rather than a value clamped to zero. It is inset from the panel edge by exactly the panel's own radius, and at that distance the inner box's corner point sits on the centre of the panel's corner arc — a right angle there is equidistant from the whole curve, which is the only shape that keeps the gap constant. Override --gog-multiselect-panel-radius and the filter reshapes itself to match.

0 selected

<gog-multiselect
  label="Country"
  [filter]="true"
  [showControls]="true"
  filterPlaceholder="Search countries…"
  filterEmptyMessage="No country matches"
  [options]="countries"
  [(value)]="selected"
/>

A long selection collapses to +N

The trigger shows what fits on one line and a count for the rest, with the full list in a tooltip. Select several of these and shrink the window to watch it re-measure: the available space changes when the container resizes, which Angular never renders for, so a ResizeObserver drives it rather than change detection.

0 selected

Custom option rows

A gogDropdownOption template replaces one option row, with the option itself plus selected, disabled and the resolved label in its context. The per-option checkbox stays — it is the control's affordance, not part of the row's content.

<gog-multiselect
  label="Reviewers"
  optionLabel="profile.fullName"
  optionValue="uuid"
  [options]="users"
  [(value)]="reviewerIds"
>
  <ng-template gogDropdownOption let-user let-label="label">
    <strong>{{ label }}</strong>
    <small>{{ user.profile.role }}</small>
  </ng-template>
</gog-multiselect>

Virtualized options — virtualize21.13.0

[virtualize]="true" renders only the rows in view. Unwindowed, 10 000 options build 10 000 DOM rows to show about six — measured in Chrome, 512 ms before the panel appears, against 21 ms windowed on the same data. The scrollbar looks the same either way, because spacers stand in for the rows that are not there.

10 000 options with a search box and the select-all row. The selection, the select-all row and the summary chips all count the whole list, not the window.

<gog-multiselect
  label="Cities"
  [options]="cities"
  [virtualize]="true"
  [filter]="true"
  [showControls]="true"
  [(value)]="windowedCities"
/>

Off by default, and never switched on at some row count. A windowed list behaves differently in ways nothing about the data predicts: Ctrl+F finds only the rendered rows, and CSS targeting :last-child matches the last rendered row. A threshold would make that depend on how much data happened to arrive, so set it per field, or app-wide with GOG_CONFIG.dropdown.virtualize.

  • The announced count stays honest: the aria-setsize and aria-posinset carry the real list, so a screen reader hears "10 000 items" rather than "20". Arrow keys walk the whole list — End reaches the last option.
  • Scrolling a keyboard-focused row out of view hands focus back to the trigger, because the row it was on no longer exists. Escape and ArrowDown both work from there. A plain list never has to do this.
  • gog-table's virtualize is a different story under the same name: table rows vary in height, so it measures as it goes — see the Table page.

API Reference

Inputs

NameTypeDefaultDescription
selectAllLabel21.3.2string | undefined'Select all'Visible text of the panel's select-all button (shown when showControls is on). Also via GOG_CONFIG.labels.selectAll.
clearAllLabel21.3.2string | undefined'Clear'The clear-all button next to it. Also via GOG_CONFIG.labels.clearAll.
value(string | number)[] (model)[]Two-way bindable selected option ids via [(value)]. Also driven by Angular Forms through writeValue/registerOnChange when used with formControlName/[formControl]/ngModel.
labelstring''Field label.
ariaLabelstring''Accessible name for the field when there is no visible label.
placeholderstring'Select...'Text shown while no option is selected.
optionsTOption[][]The list of choices — your own objects. GogDropdownOption ({ id, name, disabled? }) is just the shape the default accessors expect, not a requirement.
optionLabelstring | ((o: TOption) => string)'name'How an option turns into its visible text: a property path (dot-paths included) or a function.
optionValuestring | ((o: TOption) => unknown) | null'id'How an option turns into an emitted value. Set it to null and the control emits the option OBJECTS themselves.
optionDisabledstring | ((o: TOption) => boolean)'disabled'Which options cannot be picked.
clearablebooleanGOG_CONFIG.control.clearable ?? trueAdds a clear button in the outermost trailing position. Deliberately the one control that defaults to true: gog-multiselect shipped a clear button before this input existed, and defaulting it to false would have silently removed it.
clearAriaLabelstring'Clear selection'Accessible name for that clear button.
filterbooleanGOG_CONFIG.dropdown.filter ?? falsePuts a search box in the panel, matching case-insensitively on the resolved optionLabel. "Select all" then takes only the VISIBLE options, so it means what it says while a filter is active.
filterPosition'top' | 'bottom'GOG_CONFIG.dropdown.filterPosition ?? 'top'Which end of the panel the search box sticks to — the same vocabulary as controlsPosition, rather than a second one for the same idea.
filterPlaceholder / filterEmptyMessagestring'Search...' / 'No matches'Wording for the search box and for the empty result.
filterMatch((option: TOption, query: string) => boolean) | nullnullReplaces the default case-insensitive substring match with your own predicate.
floatLabel'none' | 'in' | 'on' | 'over'GOG_CONFIG.floatLabel.variant ?? 'none'Rests the label inside the field like a placeholder and floats it up once the selection is non-empty or the field has focus.
floatLabelShowPlaceholderbooleanGOG_CONFIG.floatLabel.showPlaceholder ?? falseReveals the placeholder once the label has floated out of the way.
minWidthstring | nullnull (--gog-multiselect-min-width, 120px)Floor for an auto-width trigger, any CSS length — so a short selection cannot collapse the field to its own chrome.
showControlsbooleanfalseShows a "select all" / "clear" row above (or below) the option list.
controlsPosition'top' | 'bottom''top'Where the select-all/clear row sits relative to the option list. Sticky either way, so it stays visible while a long list scrolls.
errorMessagestring''Error text to display. Visibility is governed by errorDisplay.
errorDisplay'auto' | 'manual'GOG_CONFIG.control.errorDisplay ?? 'manual''manual': shown for as long as errorMessage is non-empty — you decide the timing. 'auto': shown once the attached FormControl is touched and invalid; falls back to manual without one.
size'xsm' | 'sm' | 'md' | 'lg' | 'slg'GOG_CONFIG.control.size ?? 'md'Field height, padding, and font size.
disabledbooleanfalseDisables the trigger and closes the panel if it is open.
fullWidthbooleantrueFills its container by default. Set false to shrink to fit the selected summary instead.
dropdownDirection'auto' | 'up' | 'down'GOG_CONFIG.dropdown.direction ?? 'auto'Which side the panel opens on. 'auto' flips to whichever side has room in the viewport.
dropdownZIndexnumber | nullnullExplicit stacking order for the panel. Left unset it falls back to the --gog-dropdown-z token.
dropdownWidthstring | nullnullFixed panel width, any CSS length. Applies only with appendToBody. Left unset, the panel sizes to its own content with the trigger width as a floor, capped by --gog-{select,multiselect}-panel-max-width — so picking a short option no longer cuts the longer ones off the list.
dropdownMaxHeightstring | nullnullFixed panel max-height, any CSS length. Applies only with appendToBody.
appendToBodybooleanGOG_CONFIG.dropdown.appendToBody ?? falsePortals the panel into document.body instead of rendering it inline — escapes an ancestor's scroll/overflow clipping.
ripple21.6.1boolean | undefinedundefinedPress ripple on each option row in the panel. Unset, falls back to GOG_CONFIG.ripple.enabled, which is off by default; setting it here wins over the app-wide value in both directions.
virtualize21.13.0boolean | undefinedGOG_CONFIG.dropdown.virtualize ?? falseRenders only the option rows in view — about twenty in the DOM whatever the list holds. Off by default and never switched on at a row count: Ctrl+F finds only rendered rows and :last-child matches the last rendered one. aria-setsize/aria-posinset keep the announced count real.

Content slots

DirectiveContextDescription
gogDropdownOption$implicit, selected, disabled, labelReplaces one option row. The per-option checkbox stays.
gogDropdownChevron—Replaces the trigger's arrow.
gogMultiselectClearIcon—Replaces the clear control's icon.

Global Configuration

Set any of these once with provideGogConfig() instead of repeating them on every instance — an instance's own input still wins when it sets one itself. See Global Configuration for the full reference.

  • GOG_CONFIG.control.size
  • GOG_CONFIG.control.errorDisplay
  • GOG_CONFIG.control.clearable
  • GOG_CONFIG.floatLabel.variant
  • GOG_CONFIG.floatLabel.showPlaceholder
  • GOG_CONFIG.dropdown.appendToBody
  • GOG_CONFIG.dropdown.direction
  • GOG_CONFIG.dropdown.virtualize
  • GOG_CONFIG.dropdown.filter
  • GOG_CONFIG.dropdown.filterPosition
  • GOG_CONFIG.ripple.enabled — on the panel’s options
  • GOG_CONFIG.labels.clearSelection
  • GOG_CONFIG.labels.selectAll
  • GOG_CONFIG.labels.clearAll

This site is not on the default here. The library ships ripple.enabled as false; these docs set it to true app-wide so the demos above actually show the press feedback. In a fresh app you get no ripple until you ask for one — the droplet button in the header switches this site between the two, and it is on right now.

Styling Tokens

Every CSS custom property the multiselect paints with. Override any of them — on a single instance, a subtree, or a theme — to restyle it. See the Theming guide for the full token-layering model, or the Theme Generator to tweak these live.

TokenDescription
--gog-multiselect-label-colorField label color.
--gog-multiselect-field-bg / -field-borderField surface and border.
--gog-multiselect-radius / -min-widthThe field’s corner radius — the field only, since 21.12.0 — and the floor an auto-width trigger cannot collapse past.
--gog-multiselect-focus-border / -focus-ringFocus state.
--gog-multiselect-panel-bg / -panel-border / -panel-shadow / -panel-max-widthDropdown panel surface, and the cap on a panel that sizes to its own content rather than to the trigger.
--gog-multiselect-panel-gapGap between the trigger and the panel. It was --gog-multiselect-panel-offset until 21.13.0, and the old name stopped resolving in 21.14.0.
--gog-multiselect-option-heightA seed for the first frame, not the row height: the component measures a real row and corrects itself, which is also what decides whether the panel opens up or down.
--gog-multiselect-panel-radiusThe panel’s own corner, new in 21.12.0 and defaulting to var(--gog-radius). Splitting it off the field’s radius is why shaping the trigger no longer reshapes the overlay; the option row and the filter input both stay concentric with whatever you set here.
--gog-multiselect-option-hover-bg / -option-press-bg / -option-colorOption row, default, hover and pressed.
--gog-multiselect-checkbox-border / -checkbox-checked-bgPer-option selection mark. The mark is a glyph, so -checkbox-bg and -checkbox-checked-color had nothing to paint and were removed in 21.13.0.
--gog-multiselect-float-label-reserve / -in-top / -on-bg / -over-gap / -over-reserveFloat-label geometry, derived from the shared --gog-field-float-label-* scale.