Multi-select list
<gui-multi-list> is a listbox to pick any number of items. It is a
gui-list whose value is a list: each pick adds an item to
values, or takes it out. Like gui-list, it handles the keyboard, the focus and the scrolling,
renders only the items in view, and leaves drawing the items to you.
For a field the user types in to search, with the list in a popup, the options drawn for you and
the picked items as pills, use gui-multi-dropdown.
import { useState } from 'react';import { GuiLabel, GuiMultiList } from '@golemui/gui-components/react';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const languages = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese',];
export function LanguagesField() { const [values, setValues] = useState<OptionValue[]>(['Catalan']); const [visibleItems, setVisibleItems] = useState<GuiVisibleItem[]>([]);
return ( <> <div className="gui-field"> <GuiLabel uid="languages" label="Languages you speak" /> <div className="gui-widget"> <GuiMultiList id="languages" uid="languages" label="Languages you speak" height={200} items={languages} values={values} onGuiVisibleItemsChange={(event) => setVisibleItems(event.detail) } onGuiChange={(event) => setValues(event.detail.value ?? [])} > {/* The list draws no items: it reports the ones to render, and you add them as its children. One option per item, from its id and state. A click on it adds the item or takes it out. */} {visibleItems.map((item) => { const classes = ['gui-list__item']; if (item.selected) classes.push('gui-list__item-selected'); if (item.focused) classes.push('gui-list__item-focused'); return ( <div key={item.value} id={item.id} role="option" aria-selected={item.selected} className="gui-list__item-wrapper" style={{ height: 40 }} > <div className={classes.join(' ')}> <span className="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span>{String(item.template)}</span> </div> </div> ); })} </GuiMultiList> </div> </div> <p role="status">Selected: {values.join(', ') || 'none'}</p> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiMultiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';
type ListEvent<K extends keyof typeof GuiMultiListEvents> = GuiEventMap<typeof GuiMultiListEvents>[K];
@Component({ selector: 'app-languages-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-field"> <gui-label uid="languages" label="Languages you speak"></gui-label> <div class="gui-widget"> <gui-multi-list id="languages" uid="languages" label="Languages you speak" height="200" [items]="languages" [values]="values()" (gui-visible-items-change)="onVisibleItemsChange($event)" (gui-change)="onChange($event)" > <!-- One option per item, from its id and state. A click on it adds the item or takes it out. --> @for (item of visibleItems(); track item.value) { <div [id]="item.id" role="option" [attr.aria-selected]="item.selected" class="gui-list__item-wrapper" [style.height.px]="40" > <div class="gui-list__item" [class.gui-list__item-selected]="item.selected" [class.gui-list__item-focused]="item.focused" > <span class="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span>{{ item.template }}</span> </div> </div> } </gui-multi-list> </div> </div> <p role="status">Selected: {{ values().join(', ') || 'none' }}</p> `,})export class LanguagesFieldComponent { languages = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese', ];
values = signal<OptionValue[]>(['Catalan']); visibleItems = signal<GuiVisibleItem[]>([]);
// The list draws no items: it reports the ones to render, and you render // them as its children. onVisibleItemsChange(event: Event) { this.visibleItems.set( (event as ListEvent<'gui-visible-items-change'>).detail, ); }
onChange(event: Event) { this.values.set((event as ListEvent<'gui-change'>).detail.value ?? []); }}<script setup lang="ts">import { ref } from 'vue';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
const languages = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese',];
const values = ref<OptionValue[]>(['Catalan']);// The list draws no items: it reports the ones to render, and you add them as its children.const visibleItems = ref<GuiVisibleItem[]>([]);</script>
<template> <div class="gui-field"> <gui-label uid="languages" label="Languages you speak"></gui-label> <div class="gui-widget"> <gui-multi-list id="languages" uid="languages" label="Languages you speak" :height="200" :items.prop="languages" :values.prop="values" @gui-visible-items-change="visibleItems = $event.detail" @gui-change="values = $event.detail.value ?? []" > <!-- One option per item, from its id and state. A click on it adds the item or takes it out. --> <div v-for="item in visibleItems" :key="item.value" :id="item.id" role="option" :aria-selected="item.selected" class="gui-list__item-wrapper" :style="{ height: '40px' }" > <div class="gui-list__item" :class="{ 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, }" > <span class="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span>{{ item.template }}</span> </div> </div> </gui-multi-list> </div> </div> <p role="status">Selected: {{ values.join(', ') || 'none' }}</p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import { classMap } from 'lit/directives/class-map.js';import { repeat } from 'lit/directives/repeat.js';import { styleMap } from 'lit/directives/style-map.js';import type { GuiEventMap, GuiMultiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
type ListEvent<K extends keyof typeof GuiMultiListEvents> = GuiEventMap<typeof GuiMultiListEvents>[K];
const languages = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese',];
@customElement('languages-field')export class LanguagesField extends LitElement { @state() private values: OptionValue[] = ['Catalan']; @state() private visibleItems: GuiVisibleItem[] = [];
protected override createRenderRoot() { return this; }
// The list draws no items: it reports the ones to render, and you render // them as its children. private onVisibleItemsChange(event: ListEvent<'gui-visible-items-change'>) { this.visibleItems = event.detail; }
private onChange(event: ListEvent<'gui-change'>) { this.values = event.detail.value ?? []; }
override render() { return html` <div class="gui-field"> <gui-label uid="languages" label="Languages you speak"></gui-label> <div class="gui-widget"> <gui-multi-list id="languages" uid="languages" label="Languages you speak" height="200" .items=${languages} .values=${this.values} @gui-visible-items-change=${this.onVisibleItemsChange} @gui-change=${this.onChange} > ${repeat( this.visibleItems, (item) => item.value, // One option per item, from its id and state. A click on it adds // the item or takes it out. (item) => html` <div id=${item.id} role="option" aria-selected=${item.selected} class="gui-list__item-wrapper" style=${styleMap({ height: '40px' })} > <div class=${classMap({ 'gui-list__item': true, 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, })} > <span class="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span>${item.template}</span> </div> </div> `, )} </gui-multi-list> </div> </div> <p role="status">Selected: ${this.values.join(', ') || 'none'}</p> `; }}<div class="gui-field"> <gui-label uid="languages" label="Languages you speak"></gui-label> <div class="gui-widget"> <gui-multi-list id="languages" uid="languages" label="Languages you speak" height="200" ></gui-multi-list> </div></div><p id="languages-status" role="status"></p>
<template id="language-item"> <div class="gui-list__item"> <span class="gui-list__item-check" aria-hidden="true"> <svg viewBox="0 0 16 16" width="12" height="12"> <path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" /> </svg> </span> <span></span> </div></template>
<script type="module"> const list = document.querySelector('#languages'); // Each item is a copy of the template: no HTML strings, so Trusted Types has nothing to check. const itemTemplate = document.querySelector('#language-item');
// The list draws no items: it reports the ones to render, and you add them as its children. list.addEventListener('gui-visible-items-change', (event) => { list.querySelectorAll('[role="option"]').forEach((option) => option.remove()); list.prepend(...event.detail.map(renderOption)); });
// One option per item, from its id and state. A click on it adds the item or takes it out. function renderOption(item) { const option = document.createElement('div'); option.id = item.id; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', String(item.selected)); option.className = 'gui-list__item-wrapper'; option.style.height = '40px'; option.append(itemTemplate.content.firstElementChild.cloneNode(true)); option.firstChild.classList.toggle('gui-list__item-selected', item.selected); option.firstChild.classList.toggle('gui-list__item-focused', item.focused); option.querySelector('span:last-child').textContent = item.template; return option; }
list.values = ['Catalan'];
const showStatus = () => { const values = list.values ?? []; document.querySelector('#languages-status').textContent = `Selected: ${values.join(', ') || 'none'}`; }; list.addEventListener('gui-change', showStatus); showStatus();
list.items = [ 'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch', 'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi', 'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili', 'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese', ];</script>The element’s entry point is @golemui/gui-components/multi-list, which registers
<gui-multi-list>, and <gui-list>, which it builds on. The examples also use
gui-label for a visible label. To install the package and load
the stylesheet, see Installation.
The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.
import { useState } from 'react';import { GuiMultiList } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
const options = ['English', 'Portuguese', 'Spanish'];
export function LanguagesField() { const [languages, setLanguages] = useState([] as string[]);
return ( <GuiMultiList label="Languages you speak" name="languages" items={options} values={languages} onGuiChange={(event) => setLanguages(event.detail.value ?? [] as string[])} /> );}Add CUSTOM_ELEMENTS_SCHEMA to the component that uses the tags, and the stylesheet to styles in angular.json.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiEventMap, GuiMultiListEvents } from '@golemui/gui-components';import '@golemui/gui-components/multi-list';
type ChangeEvent = GuiEventMap<typeof GuiMultiListEvents>['gui-change'];
@Component({ selector: 'app-languages-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-multi-list label="Languages you speak" name="languages" [items]="options" [values]="languages" (gui-change)="onChange($event)" ></gui-multi-list> `,})export class LanguagesFieldComponent { options = ['English', 'Portuguese', 'Spanish'];
languages = [] as string[];
onChange(event: Event) { this.languages = (event as ChangeEvent).detail.value ?? [] as string[]; }}"styles": ["node_modules/@golemui/gui-components/lib/styles/index.css"]Tell Vue's compiler that gui-* tags are custom elements, and add the typings once for typed props and @gui-* handlers.
<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
const options = ['English', 'Portuguese', 'Spanish'];
const languages = ref([] as string[]);</script>
<template> <gui-multi-list label="Languages you speak" name="languages" :items.prop="options" :values="languages" @gui-change="languages = $event.detail.value ?? [] as string[]" /></template>vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') } },});import type {} from '@golemui/gui-components/vue';The page's stylesheet styles the gui-* elements, so it only reaches them outside a shadow root. Render the host component into the light DOM too, as below, or load the stylesheet into its shadow root.
import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
const options = ['English', 'Portuguese', 'Spanish'];
@customElement('languages-field')export class LanguagesField extends LitElement { @state() private languages = [] as string[];
protected override createRenderRoot() { return this; }
override render() { return html` <gui-multi-list label="Languages you speak" name="languages" .items=${options} .values=${this.languages} @gui-change=${(event: CustomEvent) => (this.languages = event.detail.value)} ></gui-multi-list> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-multi-list label="Languages you speak" name="languages"></gui-multi-list>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-multi-list');element.items = ['English', 'Portuguese', 'Spanish'];
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});Its value is values, an array, and gui-change reports the new array in event.detail.value.
Rendering the items
Section titled “Rendering the items”You render the options as the list’s children, exactly as for
gui-list, from the items
gui-visible-items-change reports: an item is selected when its value is in values. A click,
Enter or Space adds the item’s value to values or takes it out, and fires
gui-item-toggle with the item’s value, then gui-input and gui-change with the new values.
Item renderers shows this example in React, Angular, Vue, Lit and plain JavaScript.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
values | OptionValue[] | [] | The selected values. |
valueField
attribute value-field | string | – | For items given as objects, the key of the value. |
items | ListItemInput[] | [] | The items of the list: values, or objects whose value value-field names. The list normalizes them to { template, value } objects, the detail of gui-update-items. |
itemHeight
attribute item-height | number | – | Height of each item, in pixels. Needed to virtualize the list. |
height | number | – | Height of the scrollable list, in pixels. |
visibleItems read-only, no attribute | GuiVisibleItem[] | – | The items to render: the ones in view and five more on each side, each with its index, its option id and its state. Render one option per entry, as the list's children. |
uid | string | – | The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration. |
Form control properties
Every field has these, from the base class of the form controls.
| Property | Type | Default | Description |
|---|---|---|---|
value | OptionValue | – | The value of the selected item. |
name | string | – | The name the value is submitted under. Without it the element stays out of its form. Reflected, like a native control's: the form reads a single value's name from the attribute. |
label | string | – | The visible label, which also names the control for assistive technology. Without one the control has no accessible name; the date, time, range, tags and file fields warn about it in development builds. |
hint | string | – | Help text shown under the label and announced as the control's description. |
errors | string[] | [] | Error messages to show, typically from your own validation. They show as soon as they are set, unless touched is false. |
touched | boolean | – | Whether the user has interacted with the control. Leave it unset unless you validate on interaction: false holds errors back until it becomes true. |
required | boolean | false | The control needs a value: it is marked as required and, with a name, blocks submission. |
readOnly
attribute readonly | boolean | – | The value can be read and focused but not changed. |
disabled | boolean | – | Disabled by its own attribute or by a disabled <fieldset> around it. Setting the property sets the attribute, like a native control's. |
form read-only, no attribute | HTMLFormElement | null | – | The <form> the element belongs to, if any. |
validity read-only, no attribute | ValidityState | – | The element's validity, like a native control's. |
validationMessage read-only, no attribute | string | – | The message the browser shows for an invalid value. |
Methods
| Method | Description |
|---|---|
checkValidity(): boolean | – |
reportValidity(): boolean | – |
Events
Every event bubbles and is composed, so you can listen on any ancestor.
| Event | Description |
|---|---|
gui-item-toggle | The user toggled an item. detail.value is its value. |
gui-input | The user toggled an item. detail.value is the new values. |
gui-change | The user toggled an item. detail.value is the new values. |
gui-visible-items-change | The items to render changed: the user scrolled, items changed, or an item was selected or became the active one. detail is the new visibleItems. |
gui-blur | Focus left the control. |
gui-focus-change | The focused item changed. detail.index is its index, or -1. |
gui-range-change | The rendered items changed while scrolling. detail has the startIndex and endIndex. |
gui-update-items | The items were normalized. detail is the list of items. |
Examples
Section titled “Examples”The example above starts with a value, from the values attribute: a JSON array. Everything on
the List page applies too: items as objects with value-field,
disabled items, height and item-height, filtering, and the read-only and disabled states.
Check marks
Section titled “Check marks”The stylesheet draws a check box for an element with the gui-list__item-check class inside an
option’s text, and shows the icon in it while the option is selected. Put any icon in it, such as
the inline SVG of the example above; it takes the text color.
In a form
Section titled “In a form”With a name, <gui-multi-list> submits one entry per selected item, all under that name, and
nothing while none is selected. Read them with FormData.getAll(). required blocks the submit
until the user picks at least one. Submit the form below, then pick a couple of topics and submit
again.
import { useState, type SubmitEvent } from 'react';import { GuiButton, GuiLabel, GuiMultiList,} from '@golemui/gui-components/react';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const topics = [ 'Accessibility', 'Design systems', 'Performance', 'Web components',];
export function MultiListNativeForm() { const [values, setValues] = useState<OptionValue[]>([]); const [visibleItems, setVisibleItems] = useState<GuiVisibleItem[]>([]); const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => { event.preventDefault(); // getAll: the list submits one entry per selected item. const topics = new FormData(event.currentTarget).getAll('topics'); setResult(`Submitted ${JSON.stringify({ topics })}`); };
return ( <form id="topics-form" onSubmit={onSubmit}> <div className="gui-grid"> <div className="gui-grid__cell"> <div className="gui-field"> <GuiLabel uid="topics" label="Topics to follow" required /> <div className="gui-widget"> <GuiMultiList id="topics" uid="topics" label="Topics to follow" name="topics" height={160} required items={topics} values={values} onGuiVisibleItemsChange={(event) => setVisibleItems(event.detail) } onGuiChange={(event) => setValues(event.detail.value ?? [])} > {visibleItems.map((item) => { const classes = ['gui-list__item']; if (item.selected) classes.push('gui-list__item-selected'); if (item.focused) classes.push('gui-list__item-focused'); return ( <div key={item.value} id={item.id} role="option" aria-selected={item.selected} className="gui-list__item-wrapper" style={{ height: 40 }} > <div className={classes.join(' ')}> {String(item.template)} </div> </div> ); })} </GuiMultiList> </div> </div> </div> <div className="gui-grid__cell"> <GuiButton type="submit" label="Follow" /> </div> </div> <p id="topics-result" role="status"> {result} </p> </form> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiMultiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';
type ListEvent<K extends keyof typeof GuiMultiListEvents> = GuiEventMap< typeof GuiMultiListEvents>[K];
@Component({ selector: 'app-multi-list-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="topics-form" #form (submit)="onSubmit($event, form)"> <div class="gui-grid"> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="topics" label="Topics to follow" required ></gui-label> <div class="gui-widget"> <gui-multi-list id="topics" uid="topics" label="Topics to follow" name="topics" height="160" required [items]="topics" [values]="values()" (gui-visible-items-change)="onVisibleItemsChange($event)" (gui-change)="onChange($event)" > @for (item of visibleItems(); track item.value) { <div [id]="item.id" role="option" [attr.aria-selected]="item.selected" class="gui-list__item-wrapper" [style.height.px]="40" > <div class="gui-list__item" [class.gui-list__item-selected]="item.selected" [class.gui-list__item-focused]="item.focused" > {{ item.template }} </div> </div> } </gui-multi-list> </div> </div> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Follow"></gui-button> </div> </div> <p id="topics-result" role="status">{{ result() }}</p> </form> `,})export class MultiListNativeFormComponent { topics = ['Accessibility', 'Design systems', 'Performance', 'Web components'];
values = signal<OptionValue[]>([]); visibleItems = signal<GuiVisibleItem[]>([]); result = signal('');
onVisibleItemsChange(event: Event) { this.visibleItems.set( (event as ListEvent<'gui-visible-items-change'>).detail, ); }
onChange(event: Event) { this.values.set((event as ListEvent<'gui-change'>).detail.value ?? []); }
onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); // getAll: the list submits one entry per selected item. const topics = new FormData(form).getAll('topics'); this.result.set(`Submitted ${JSON.stringify({ topics })}`); }}<script setup lang="ts">import { ref } from 'vue';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
const topics = [ 'Accessibility', 'Design systems', 'Performance', 'Web components',];
const values = ref<OptionValue[]>([]);const visibleItems = ref<GuiVisibleItem[]>([]);const result = ref('');
function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; // getAll: the list submits one entry per selected item. const topics = new FormData(event.currentTarget).getAll('topics'); result.value = `Submitted ${JSON.stringify({ topics })}`;}</script>
<template> <form id="topics-form" @submit.prevent="onSubmit"> <div class="gui-grid"> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="topics" label="Topics to follow" :required="true" ></gui-label> <div class="gui-widget"> <gui-multi-list id="topics" uid="topics" label="Topics to follow" name="topics" :height="160" :required="true" :items.prop="topics" :values.prop="values" @gui-visible-items-change="visibleItems = $event.detail" @gui-change="values = $event.detail.value ?? []" > <div v-for="item in visibleItems" :key="item.value" :id="item.id" role="option" :aria-selected="item.selected" class="gui-list__item-wrapper" :style="{ height: '40px' }" > <div class="gui-list__item" :class="{ 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, }" > {{ item.template }} </div> </div> </gui-multi-list> </div> </div> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Follow"></gui-button> </div> </div> <p id="topics-result" role="status">{{ result }}</p> </form></template>import { LitElement, html } from 'lit';import { customElement, query, state } from 'lit/decorators.js';import { classMap } from 'lit/directives/class-map.js';import { repeat } from 'lit/directives/repeat.js';import { styleMap } from 'lit/directives/style-map.js';import type { GuiEventMap, GuiMultiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/label';import '@golemui/gui-components/multi-list';import '@golemui/gui-components/index.css';
type ListEvent<K extends keyof typeof GuiMultiListEvents> = GuiEventMap< typeof GuiMultiListEvents>[K];
const topics = [ 'Accessibility', 'Design systems', 'Performance', 'Web components',];
@customElement('multi-list-native-form')export class MultiListNativeForm extends LitElement { @query('#topics-form') private form!: HTMLFormElement; @state() private values: OptionValue[] = []; @state() private visibleItems: GuiVisibleItem[] = []; @state() private result = '';
protected override createRenderRoot() { return this; }
private onVisibleItemsChange(event: ListEvent<'gui-visible-items-change'>) { this.visibleItems = event.detail; }
private onChange(event: ListEvent<'gui-change'>) { this.values = event.detail.value ?? []; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); // getAll: the list submits one entry per selected item. const topics = new FormData(this.form).getAll('topics'); this.result = `Submitted ${JSON.stringify({ topics })}`; }
override render() { return html` <form id="topics-form" @submit=${this.onSubmit}> <div class="gui-grid"> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="topics" label="Topics to follow" required ></gui-label> <div class="gui-widget"> <gui-multi-list id="topics" uid="topics" label="Topics to follow" name="topics" height="160" required .items=${topics} .values=${this.values} @gui-visible-items-change=${this.onVisibleItemsChange} @gui-change=${this.onChange} > ${repeat( this.visibleItems, (item) => item.value, (item) => html` <div id=${item.id} role="option" aria-selected=${item.selected} class="gui-list__item-wrapper" style=${styleMap({ height: '40px' })} > <div class=${classMap({ 'gui-list__item': true, 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, })} > ${item.template} </div> </div> `, )} </gui-multi-list> </div> </div> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Follow"></gui-button> </div> </div> <p id="topics-result" role="status">${this.result}</p> </form> `; }}<form id="topics-form"> <div class="gui-grid"> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="topics" label="Topics to follow" required></gui-label> <div class="gui-widget"> <gui-multi-list id="topics" uid="topics" label="Topics to follow" name="topics" height="160" required ></gui-multi-list> </div> </div> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Follow"></gui-button> </div> </div> <p id="topics-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#topics-form'); const list = document.querySelector('#topics'); list.addEventListener('gui-visible-items-change', (event) => { list.querySelectorAll('[role="option"]').forEach((option) => option.remove()); list.prepend(...event.detail.map(renderOption)); });
function renderOption(item) { const option = document.createElement('div'); option.id = item.id; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', String(item.selected)); option.className = 'gui-list__item-wrapper'; option.style.height = '40px'; const text = document.createElement('div'); text.className = 'gui-list__item'; text.classList.toggle('gui-list__item-selected', item.selected); text.classList.toggle('gui-list__item-focused', item.focused); text.textContent = item.template; option.append(text); return option; }
list.items = ['Accessibility', 'Design systems', 'Performance', 'Web components'];
form.addEventListener('submit', (event) => { event.preventDefault(); // getAll: the list submits one entry per selected item. const topics = new FormData(form).getAll('topics'); document.querySelector('#topics-result').textContent = `Submitted ${JSON.stringify({ topics })}`; });</script>How the elements submit, validate and reset is in Native forms.
gui-multi-select-trigger
Section titled “gui-multi-select-trigger”<gui-multi-select-trigger> is the field of
gui-multi-dropdown: the selected options as removable
pills, and a search input with the combobox role. It holds no value, options or filtering of its
own; the dropdown that composes it with a gui-multi-list in a popup owns them, and listens to its
gui-pill-remove event. It is a building block rather than an element to use on its own: to let
users pick several items, use gui-multi-dropdown or gui-multi-list, or
gui-tags for free text.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
touched | boolean | – | Whether that field was touched. |
required | boolean | false | Whether that field is required. |
disabled | boolean | false | Disables the trigger. |
readOnly
attribute readonly | boolean | false | Shows the selection without letting the user change it. |
errors | string[] | [] | Errors of the field the trigger belongs to. |
placeholder | string | – | Text shown while nothing is selected. |
icon | string | – | Icon class name shown inside the trigger. |
autocomplete | string | – | The autocomplete hint of the search field. |
removeAriaLabel
attribute remove-aria-label | string | – | Accessible name of each remove button. An empty value keeps the default. |
removeIcon
attribute remove-icon | string | – | Icon class name of the remove buttons. |
compactAriaLabel
attribute compact-aria-label | string | – | Accessible name of the count shown when the options do not fit. {count} is the number selected. An empty value keeps the default. |
toolbarAriaLabel
attribute toolbar-aria-label | string | – | Accessible name of the selected options. An empty value removes it. |
uid | string | – | The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration. |
Events
Every event bubbles and is composed, so you can listen on any ancestor.
| Event | Description |
|---|---|
gui-pill-remove | The user removed a selected option from its pill. detail.key is the pill's key. |
gui-dropdown-toggle | The count bubble opened or closed its dropdown. detail.open is the new state. |
CSS custom properties
| Property | Default | Description |
|---|---|---|
--gui-pill-height | – | Height of each pill. |
--gui-pill-font-size | – | Font size of the pill text. |
--gui-pill-action-size | – | Size of the icons inside a pill. |
--gui-pill-action-hit | – | Clickable area of the buttons inside a pill. |
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”Like gui-list, <gui-multi-list> has a shadow root that scrolls the items and holds them in
place. Your options are its children in the light DOM, so plain CSS styles them:
<gui-multi-list id="languages" uid="languages" role="listbox" tabindex="0" aria-label="Languages you speak" aria-multiselectable="true" aria-activedescendant="languages-item-2"> <!-- Shadow root: the scrolling box, which shows the items through its slots. --> <div id="languages-item-1" role="option" class="gui-list__item-wrapper" aria-selected="false" > <div class="gui-list__item"> <span class="gui-list__item-check" aria-hidden="true"><svg>…</svg></span> <span>Chinese</span> </div> </div> <div id="languages-item-2" role="option" class="gui-list__item-wrapper" aria-selected="true" > <div class="gui-list__item gui-list__item-selected gui-list__item-focused"> <span class="gui-list__item-check" aria-hidden="true"><svg>…</svg></span> <span>English</span> </div> </div> <!-- One option per item in range, then the errors, as in gui-list. --></gui-multi-list>The classes are those of gui-list, and one more:
| Class | Part |
|---|---|
gui-list__item-check | The check box of an option. Its icon shows while the option’s text has gui-list__item-selected. |
The shadow root has no parts to style and no tokens of its own: style the gui-multi-list
element and your options from your stylesheet.
Tokens
Section titled “Tokens”The list reads the same tokens as gui-list, and the
check box reads two more:
| Token | What it styles |
|---|---|
--gui-radius-md | The check box’s corner radius. |
--gui-space-2 | The space between the check box and the text. |
To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.
Accessibility
Section titled “Accessibility”- Listbox. The element itself is the listbox, with
role="listbox"andaria-multiselectable="true", and takes the focus. The active item is announced througharia-activedescendant. - Name.
labelnames the listbox througharia-label, andhintdescribes it througharia-description. Without a label the list has no accessible name. - Errors and states. As on
gui-list:aria-invalidandaria-errormessagewhile errors show, andaria-required,aria-readonlyandaria-disabled. - Options. Set
aria-selectedon each option you render fromvalues, and hide the check icon from screen readers:aria-selectedalready says the option is picked.
| Key | Action |
|---|---|
| Tab | Moves focus to the list, on the first selected item, and away from it. |
| ↓ ↑ | Moves to the next or previous item, skipping disabled ones. |
| Page Down Page Up | Moves by the number of items that fit in the list. |
| Home End | Moves to the first or last item that isn’t disabled. |
| Enter Space | Selects the active item, or unselects it, and fires gui-change. |
What every element shares, such as translating the browser messages, is in Accessibility.