Skip to content
Components, switch product

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.

Code of the example above
MultiDropdownBasic.tsx
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>
</>
);
}

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.

LanguagesField.tsx
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[])}
/>
);
}

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.

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.

renderItem draws the content of each option, as on the Dropdown. The check box stays: the content goes next to it.

Code of the example above
MultiDropdownRenderItem.tsx
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>
</>
);
}

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.

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.

Code of the example above
MultiDropdownDisabledReadonly.tsx
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>
);
}

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.

Code of the example above
MultiDropdownNativeForm.tsx
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>
);
}

How the elements submit, validate and reset is in Native forms.

<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
<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:

ClassPart
gui-multi-dropdownThe element itself.
gui-multi-select__fieldThe box of the field, around the pills and the text field.
gui-multi-select__pillsThe pills.
gui-pills__pillOne pill, a button that removes its item.
gui-pills__pill-textThe text of a pill.
gui-pills__pill-removeThe remove icon of a pill.
gui-pills__countThe count shown when the pills don’t fit. It opens them in a list.
gui-multi-select__inputThe text field the user types in.
gui-list__item-checkThe 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.

Everything on the Dropdown applies, with these differences:

  • Listbox. The list has aria-multiselectable="true", and every picked option aria-selected="true".
  • Pills. The pills are a toolbar named Selected options. Each pill is a button named by its item, described as Remove option. Rename them with remove-aria-label, or translate them under the keys selectedOptions and removeOption.
  • Count. When the pills fold, the count is named {count} selected, under the key selectedCount.
KeyAction
Enter Space in the listAdds the active option to values, or takes it out. The list stays open.
← at the start of the fieldMoves into the pills, on the last one.
Backspace in an empty fieldMoves into the pills, on the last one, without removing it.
← → in the pillsMoves between the pills. Past the last one, back to the field.
Delete Backspace on a pillRemoves its item.

What every element shares, such as translating the browser messages, is in Accessibility.