Multi-select dropdown
<gui-multi-dropdown> picks any number of items out of many. It is a
gui-dropdown whose value is a list: each pick adds an item
to values, or takes it out, and the field shows the picked items as pills the user can remove.
The list stays open while the user picks, and keeps the search.
import { useState } from 'react';import { GuiMultiDropdown } from '@golemui/gui-components/react';import type { 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 MultiDropdownBasic() { const [values, setValues] = useState<OptionValue[]>(['English']); const [status, setStatus] = useState('');
return ( <> <GuiMultiDropdown id="languages" label="Languages you speak" placeholder="Type to search" values={values} items={languages} onGuiChange={(event) => { const selected = event.detail.value ?? []; setValues(selected); setStatus( selected.length ? `Selected: ${selected.join(', ')}` : 'No languages selected', ); }} /> <p id="languages-status" role="status"> {status} </p> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiMultiDropdownEvents, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/multi-dropdown';
type DropdownEvent<K extends keyof typeof GuiMultiDropdownEvents> = GuiEventMap< typeof GuiMultiDropdownEvents>[K];
@Component({ selector: 'app-multi-dropdown-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-multi-dropdown id="languages" label="Languages you speak" placeholder="Type to search" [values]="values()" [items]="languages" (gui-change)="onChange($event)" ></gui-multi-dropdown> <p id="languages-status" role="status">{{ status() }}</p> `,})export class MultiDropdownBasicComponent { 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[]>(['English']); status = signal('');
onChange(event: Event) { const languages = (event as DropdownEvent<'gui-change'>).detail.value ?? []; this.values.set(languages); this.status.set( languages.length ? `Selected: ${languages.join(', ')}` : 'No languages selected', ); }}<script setup lang="ts">import { ref } from 'vue';import type { OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/multi-dropdown';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[]>(['English']);const status = ref('');
function onChange(languages: OptionValue[] = []) { values.value = languages; status.value = languages.length ? `Selected: ${languages.join(', ')}` : 'No languages selected';}</script>
<template> <gui-multi-dropdown id="languages" label="Languages you speak" placeholder="Type to search" :items.prop="languages" :values.prop="values" @gui-change="onChange($event.detail.value)" ></gui-multi-dropdown> <p id="languages-status" role="status">{{ status }}</p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiMultiDropdownEvents, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/multi-dropdown';import '@golemui/gui-components/index.css';
type DropdownEvent<K extends keyof typeof GuiMultiDropdownEvents> = GuiEventMap< typeof GuiMultiDropdownEvents>[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('multi-dropdown-basic')export class MultiDropdownBasic extends LitElement { @state() private values: OptionValue[] = ['English']; @state() private status = '';
protected override createRenderRoot() { return this; }
private onChange(event: DropdownEvent<'gui-change'>) { this.values = event.detail.value ?? []; this.status = this.values.length ? `Selected: ${this.values.join(', ')}` : 'No languages selected'; }
override render() { return html` <gui-multi-dropdown id="languages" label="Languages you speak" placeholder="Type to search" .values=${this.values} .items=${languages} @gui-change=${this.onChange} ></gui-multi-dropdown> <p id="languages-status" role="status">${this.status}</p> `; }}<gui-multi-dropdown id="languages" label="Languages you speak" placeholder="Type to search" values='["English"]'></gui-multi-dropdown><p id="languages-status" role="status"></p>
<script type="module"> const dropdown = document.querySelector('#languages');
dropdown.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', ];
dropdown.addEventListener('gui-change', (event) => { const languages = event.detail.value; document.querySelector('#languages-status').textContent = languages.length ? `Selected: ${languages.join(', ')}` : 'No languages selected'; });</script>The element’s entry point is @golemui/gui-components/multi-dropdown, which registers
<gui-multi-dropdown> and the gui-multi-list it opens. 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 { GuiMultiDropdown } 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 ( <GuiMultiDropdown 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, GuiMultiDropdownEvents } from '@golemui/gui-components';import '@golemui/gui-components/multi-dropdown';
type ChangeEvent = GuiEventMap<typeof GuiMultiDropdownEvents>['gui-change'];
@Component({ selector: 'app-languages-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-multi-dropdown label="Languages you speak" name="languages" [items]="options" [values]="languages" (gui-change)="onChange($event)" ></gui-multi-dropdown> `,})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-dropdown';import '@golemui/gui-components/index.css';
const options = ['English', 'Portuguese', 'Spanish'];
const languages = ref([] as string[]);</script>
<template> <gui-multi-dropdown 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-dropdown';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-dropdown label="Languages you speak" name="languages" .items=${options} .values=${this.languages} @gui-change=${(event: CustomEvent) => (this.languages = event.detail.value)} ></gui-multi-dropdown> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-multi-dropdown label="Languages you speak" name="languages"></gui-multi-dropdown>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/multi-dropdown';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-multi-dropdown');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.
The example above starts with a value, from the values attribute: a JSON array.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
values | OptionValue[] | [] | The values of the selected items. |
removeAriaLabel
attribute remove-aria-label | string | – | Accessible name of each pill's remove button. An empty value keeps the default. |
removeIcon
attribute remove-icon | string | – | Icon class name of the pills' remove buttons. |
items | ListItemInput[] | [] | The items: values, which are both the text and the value, or objects read through label-field and value-field. An object with disabled: true can't be picked. |
labelField
attribute label-field | string | – | For items given as objects, the key of the text to show. label by default. |
valueField
attribute value-field | string | – | For items given as objects, the key of the value. value by default. |
searchFields
attribute search-fields | string[] | – | For items given as objects, the keys the search looks in. By default label-field and value-field, or every key when neither is set. |
remoteFilter
attribute remote-filter | boolean | false | The search is the app's: the dropdown shows items as they are and reports what the user types with gui-filter. |
inputDebounce
attribute input-debounce | number | 500 | Milliseconds to wait after the last keystroke before searching. |
renderItem property only | GuiItemRenderer | – | Renders the content of each option, instead of its text. It gets the item and the option's state with Lit's html, and returns a template, a text or a node. |
placeholder | string | – | Text shown while the field is empty. |
icon | string | – | Icon class name shown inside the control, for example from an icon font. |
autocomplete | string | – | The autocomplete hint passed to the inner text field. |
toggleAriaLabel
attribute toggle-aria-label | string | – | Accessible name of the button that opens the list. An empty value keeps the default. |
itemHeight
attribute item-height | number | – | Height of each item, in pixels. |
height | number | – | Height of the list, in pixels. It scrolls past 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. |
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-input | The user picked or removed an item. detail.value is the new values. |
gui-change | The user picked or removed an item. detail.value is the new values. |
gui-blur | Focus left the control. |
gui-filter | The user typed to search the items, after input-debounce. detail.query is the text, '' when the search is cleared. |
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. |
Examples
Section titled “Examples”Everything on the Dropdown page applies too: items as
objects with label-field, value-field and search-fields, searching on your server with
remote-filter and gui-filter, long lists, the icon, and the read-only and disabled states.
Custom options
Section titled “Custom options”renderItem draws the content of each option, as on the
Dropdown. The check box stays: the content goes
next to it.
import { GuiMultiDropdown } from '@golemui/gui-components/react';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
type Reviewer = { id: string; name: string; role: string };
const reviewers: Reviewer[] = [ { id: 'al', name: 'Ada Lovelace', role: 'Engineering' }, { id: 'gh', name: 'Grace Hopper', role: 'Engineering' }, { id: 'hl', name: 'Hedy Lamarr', role: 'Research' }, { id: 'kj', name: 'Katherine Johnson', role: 'Data' }, { id: 'mh', name: 'Margaret Hamilton', role: 'Engineering' }, { id: 'rp', name: 'Radia Perlman', role: 'Networking' },];
const searchFields = ['name', 'role'];
// The check box stays: the content goes next to it.const renderReviewer: GuiItemRenderer<Reviewer> = (item, { html }) => html` <span class="reviewer__name">${item.template.name}</span> <span class="reviewer__role">${item.template.role}</span>`;
const reviewerStyles = ` #reviewers .gui-list__item-content { display: flex; align-items: baseline; gap: var(--gui-space-2); }
.reviewer__role { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; }`;
export function MultiDropdownRenderItem() { return ( <> <GuiMultiDropdown id="reviewers" label="Reviewers" labelField="name" valueField="id" searchFields={searchFields} items={reviewers} renderItem={renderReviewer} /> <style>{reviewerStyles}</style> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation,} from '@angular/core';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/multi-dropdown';
type Reviewer = { id: string; name: string; role: string };
@Component({ selector: 'app-multi-dropdown-render-item', schemas: [CUSTOM_ELEMENTS_SCHEMA], // The element renders the options itself: the styles must not be scoped. encapsulation: ViewEncapsulation.None, template: ` <gui-multi-dropdown id="reviewers" label="Reviewers" label-field="name" value-field="id" [searchFields]="searchFields" [items]="reviewers" [renderItem]="renderReviewer" ></gui-multi-dropdown> `, styles: ` #reviewers .gui-list__item-content { display: flex; align-items: baseline; gap: var(--gui-space-2); }
.reviewer__role { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; } `,})export class MultiDropdownRenderItemComponent { searchFields = ['name', 'role']; reviewers: Reviewer[] = [ { id: 'al', name: 'Ada Lovelace', role: 'Engineering' }, { id: 'gh', name: 'Grace Hopper', role: 'Engineering' }, { id: 'hl', name: 'Hedy Lamarr', role: 'Research' }, { id: 'kj', name: 'Katherine Johnson', role: 'Data' }, { id: 'mh', name: 'Margaret Hamilton', role: 'Engineering' }, { id: 'rp', name: 'Radia Perlman', role: 'Networking' }, ];
// The check box stays: the content goes next to it. renderReviewer: GuiItemRenderer<Reviewer> = (item, { html }) => html` <span class="reviewer__name">${item.template.name}</span> <span class="reviewer__role">${item.template.role}</span> `;}<script setup lang="ts">import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/multi-dropdown';import '@golemui/gui-components/index.css';
type Reviewer = { id: string; name: string; role: string };
const reviewers: Reviewer[] = [ { id: 'al', name: 'Ada Lovelace', role: 'Engineering' }, { id: 'gh', name: 'Grace Hopper', role: 'Engineering' }, { id: 'hl', name: 'Hedy Lamarr', role: 'Research' }, { id: 'kj', name: 'Katherine Johnson', role: 'Data' }, { id: 'mh', name: 'Margaret Hamilton', role: 'Engineering' }, { id: 'rp', name: 'Radia Perlman', role: 'Networking' },];const searchFields = ['name', 'role'];
// The check box stays: the content goes next to it.const renderReviewer: GuiItemRenderer<Reviewer> = (item, { html }) => html` <span class="reviewer__name">${item.template.name}</span> <span class="reviewer__role">${item.template.role}</span>`;</script>
<template> <gui-multi-dropdown id="reviewers" label="Reviewers" label-field="name" value-field="id" :searchFields.prop="searchFields" :items.prop="reviewers" :renderItem.prop="renderReviewer" ></gui-multi-dropdown></template>
<style>#reviewers .gui-list__item-content { display: flex; align-items: baseline; gap: var(--gui-space-2);}
.reviewer__role { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75;}</style>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/multi-dropdown';import '@golemui/gui-components/index.css';
type Reviewer = { id: string; name: string; role: string };
const reviewers: Reviewer[] = [ { id: 'al', name: 'Ada Lovelace', role: 'Engineering' }, { id: 'gh', name: 'Grace Hopper', role: 'Engineering' }, { id: 'hl', name: 'Hedy Lamarr', role: 'Research' }, { id: 'kj', name: 'Katherine Johnson', role: 'Data' }, { id: 'mh', name: 'Margaret Hamilton', role: 'Engineering' }, { id: 'rp', name: 'Radia Perlman', role: 'Networking' },];
const searchFields = ['name', 'role'];
@customElement('multi-dropdown-render-item')export class MultiDropdownRenderItem extends LitElement { protected override createRenderRoot() { return this; }
// The check box stays: the content goes next to it. private renderReviewer: GuiItemRenderer<Reviewer> = (item, { html }) => html` <span class="reviewer__name">${item.template.name}</span> <span class="reviewer__role">${item.template.role}</span> `;
override render() { return html` <gui-multi-dropdown id="reviewers" label="Reviewers" label-field="name" value-field="id" .searchFields=${searchFields} .items=${reviewers} .renderItem=${this.renderReviewer} ></gui-multi-dropdown>
<style> #reviewers .gui-list__item-content { display: flex; align-items: baseline; gap: var(--gui-space-2); }
.reviewer__role { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; } </style> `; }}<gui-multi-dropdown id="reviewers" label="Reviewers" label-field="name" value-field="id" search-fields='["name", "role"]'></gui-multi-dropdown>
<script type="module"> const dropdown = document.querySelector('#reviewers');
dropdown.items = [ { id: 'al', name: 'Ada Lovelace', role: 'Engineering' }, { id: 'gh', name: 'Grace Hopper', role: 'Engineering' }, { id: 'hl', name: 'Hedy Lamarr', role: 'Research' }, { id: 'kj', name: 'Katherine Johnson', role: 'Data' }, { id: 'mh', name: 'Margaret Hamilton', role: 'Engineering' }, { id: 'rp', name: 'Radia Perlman', role: 'Networking' }, ];
// The check box stays: the content goes next to it. dropdown.renderItem = (item, { html }) => html` <span class="reviewer__name">${item.template.name}</span> <span class="reviewer__role">${item.template.role}</span> `;</script>
<style> #reviewers .gui-list__item-content { display: flex; align-items: baseline; gap: var(--gui-space-2); }
.reviewer__role { margin-inline-start: auto; font-size: var(--gui-font-xs); opacity: 0.75; }</style>Removing items
Section titled “Removing items”Each pill has a remove button. From the keyboard, ← at the start of the field, or Backspace in an empty field, moves into the pills; Delete or Backspace removes the focused one. When the pills don’t fit the field, they fold into a count, which opens them in a list of their own.
Disabled and read-only
Section titled “Disabled and read-only”A disabled dropdown can’t be focused or changed, and a form doesn’t submit it. A readonly
dropdown shows its pills without remove buttons and can be opened to see the items, but the user
can’t search or pick, and a form submits its values.
import { GuiMultiDropdown } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function MultiDropdownDisabledReadonly() { return ( <div className="gui-grid gui-grid--row"> <div className="gui-grid__cell"> <GuiMultiDropdown label="Regions" values={['EU']} items={['EU', 'US', 'APAC']} disabled /> </div> <div className="gui-grid__cell"> <GuiMultiDropdown label="Plans" values={['Team', 'Business']} items={['Starter', 'Team', 'Business']} readOnly /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/multi-dropdown';
@Component({ selector: 'app-multi-dropdown-disabled-readonly', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-multi-dropdown label="Regions" [values]="regions" [items]="allRegions" disabled ></gui-multi-dropdown> </div> <div class="gui-grid__cell"> <gui-multi-dropdown label="Plans" [values]="plans" [items]="allPlans" readonly ></gui-multi-dropdown> </div> </div> `,})export class MultiDropdownDisabledReadonlyComponent { regions = ['EU']; allRegions = ['EU', 'US', 'APAC']; plans = ['Team', 'Business']; allPlans = ['Starter', 'Team', 'Business'];}<script setup lang="ts">import '@golemui/gui-components/multi-dropdown';import '@golemui/gui-components/index.css';
const regions = ['EU', 'US', 'APAC'];const plans = ['Starter', 'Team', 'Business'];</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-multi-dropdown label="Regions" :values.prop="['EU']" :items.prop="regions" :disabled="true" ></gui-multi-dropdown> </div> <div class="gui-grid__cell"> <gui-multi-dropdown label="Plans" :values.prop="['Team', 'Business']" :items.prop="plans" :readOnly="true" ></gui-multi-dropdown> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/multi-dropdown';import '@golemui/gui-components/index.css';
@customElement('multi-dropdown-disabled-readonly')export class MultiDropdownDisabledReadonly extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-multi-dropdown label="Regions" .values=${['EU']} .items=${['EU', 'US', 'APAC']} disabled ></gui-multi-dropdown> </div> <div class="gui-grid__cell"> <gui-multi-dropdown label="Plans" .values=${['Team', 'Business']} .items=${['Starter', 'Team', 'Business']} readonly ></gui-multi-dropdown> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-multi-dropdown label="Regions" values='["EU"]' items='["EU", "US", "APAC"]' disabled ></gui-multi-dropdown> </div> <div class="gui-grid__cell"> <gui-multi-dropdown label="Plans" values='["Team", "Business"]' items='["Starter", "Team", "Business"]' readonly ></gui-multi-dropdown> </div></div>In a form
Section titled “In a form”With a name, <gui-multi-dropdown> submits one entry per picked item, all under that name, and
nothing while none is picked. 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, GuiMultiDropdown } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
const allTopics = [ 'Accessibility', 'Design systems', 'Performance', 'Web components',];
export function MultiDropdownNativeForm() { const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => { event.preventDefault(); // getAll: the dropdown 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 gui-grid--row"> <div className="gui-grid__cell gui-grid__cell--span-3"> <GuiMultiDropdown label="Topics to follow" name="topics" items={allTopics} required /> </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 '@golemui/gui-components/button';import '@golemui/gui-components/multi-dropdown';
@Component({ selector: 'app-multi-dropdown-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="topics-form" #form (submit)="onSubmit($event, form)"> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell gui-grid__cell--span-3"> <gui-multi-dropdown label="Topics to follow" name="topics" [items]="topics" required ></gui-multi-dropdown> </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 MultiDropdownNativeFormComponent { topics = ['Accessibility', 'Design systems', 'Performance', 'Web components']; result = signal('');
onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); // getAll: the dropdown 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 '@golemui/gui-components/button';import '@golemui/gui-components/multi-dropdown';import '@golemui/gui-components/index.css';
const topics = [ 'Accessibility', 'Design systems', 'Performance', 'Web components',];
const result = ref('');
function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; // getAll: the dropdown 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 gui-grid--row"> <div class="gui-grid__cell gui-grid__cell--span-3"> <gui-multi-dropdown label="Topics to follow" name="topics" :items.prop="topics" :required="true" ></gui-multi-dropdown> </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 '@golemui/gui-components/button';import '@golemui/gui-components/multi-dropdown';import '@golemui/gui-components/index.css';
const topics = [ 'Accessibility', 'Design systems', 'Performance', 'Web components',];
@customElement('multi-dropdown-native-form')export class MultiDropdownNativeForm extends LitElement { @query('#topics-form') private form!: HTMLFormElement; @state() private result = '';
protected override createRenderRoot() { return this; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); // getAll: the dropdown 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 gui-grid--row"> <div class="gui-grid__cell gui-grid__cell--span-3"> <gui-multi-dropdown label="Topics to follow" name="topics" .items=${topics} required ></gui-multi-dropdown> </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 gui-grid--row"> <div class="gui-grid__cell gui-grid__cell--span-3"> <gui-multi-dropdown label="Topics to follow" name="topics" items='["Accessibility", "Design systems", "Performance", "Web components"]' required ></gui-multi-dropdown> </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');
form.addEventListener('submit', (event) => { event.preventDefault(); // getAll: the dropdown 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.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<gui-multi-dropdown> renders into the light DOM. Its field is a gui-multi-select-trigger, with
the pills and the text field, and its panel holds a
gui-multi-list:
<gui-multi-dropdown class="gui-multi-dropdown gui-field"> <label class="gui-label" for="langs" id="langs_label"> <span class="gui-label__text">Languages</span> </label> <div class="gui-widget"> <gui-multi-select-trigger> <div role="group" class="gui-widget-input gui-multi-select__field"> <gui-pills class="gui-multi-select__pills"> <div class="gui-pills__strip-wrapper"> <div class="gui-pills__strip" role="toolbar" aria-label="Selected options" > <button type="button" class="gui-pills__pill" aria-label="English" aria-description="Remove option" > <span class="gui-pills__pill-text">English</span> <span class="gui-pills__pill-remove" aria-hidden="true" ><svg>…</svg></span > </button> </div> </div> <div class="gui-pills__compact"> <button type="button" class="gui-pills__count" aria-label="1 selected" > 1 </button> </div> </gui-pills> <input type="text" role="combobox" id="langs" class="gui-multi-select__input" aria-autocomplete="list" aria-expanded="true" aria-controls="langs-list" aria-labelledby="langs_label" /> </div> </gui-multi-select-trigger> <button type="button" class="gui-dropdown__arrow" aria-label="Show options"> <span class="gui-caret" aria-hidden="true"></span> </button> <div class="gui-picker__panel"> <gui-multi-list id="langs-list" role="listbox" aria-multiselectable="true" > <div id="langs-item-0" role="option" class="gui-list__item-wrapper" aria-selected="true" > <div class="gui-list__item gui-list__item-selected"> <span class="gui-list__item-check" aria-hidden="true" ><svg>…</svg></span > <div class="gui-list__item-content">English</div> </div> </div> <!-- One option per item in view, and five more on each side. --> </gui-multi-list> </div> </div> <div class="gui-validator" id="langs_errors" role="alert"></div></gui-multi-dropdown>It has the classes of the Dropdown, with these instead of the dropdown’s own:
| Class | Part |
|---|---|
gui-multi-dropdown | The element itself. |
gui-multi-select__field | The box of the field, around the pills and the text field. |
gui-multi-select__pills | The pills. |
gui-pills__pill | One pill, a button that removes its item. |
gui-pills__pill-text | The text of a pill. |
gui-pills__pill-remove | The remove icon of a pill. |
gui-pills__count | The count shown when the pills don’t fit. It opens them in a list. |
gui-multi-select__input | The text field the user types in. |
gui-list__item-check | The check box of an option. It shows its icon while the option is selected. |
The pills read --gui-pill-height, --gui-pill-font-size, --gui-pill-action-size and
--gui-pill-action-hit on top of the Dropdown’s tokens.
To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.
Accessibility
Section titled “Accessibility”Everything on the Dropdown applies, with these differences:
- Listbox. The list has
aria-multiselectable="true", and every picked optionaria-selected="true". - Pills. The pills are a toolbar named
Selected options. Each pill is a button named by its item, described asRemove option. Rename them withremove-aria-label, or translate them under the keysselectedOptionsandremoveOption. - Count. When the pills fold, the count is named
{count} selected, under the keyselectedCount.
| Key | Action |
|---|---|
| Enter Space in the list | Adds the active option to values, or takes it out. The list stays open. |
| ← at the start of the field | Moves into the pills, on the last one. |
| Backspace in an empty field | Moves into the pills, on the last one, without removing it. |
| ← → in the pills | Moves between the pills. Past the last one, back to the field. |
| Delete Backspace on a pill | Removes its item. |
What every element shares, such as translating the browser messages, is in Accessibility.