Multi Dropdown
Multi Dropdown widgets are Input Fields that let the user pick any number of items from a searchable list. Selected items render as removable pills to the left of the filter input, and the value bound at the widget’s path is an array of the selected item values. The panel stays open while the user toggles items and the filter text survives a toggle, so picking several matches takes one search. Pills keep their labels even when the filter hides their items. For a single value, use the Dropdown; for an always-visible list without a filter, use the Multi List.
The input is a combobox. ArrowDown opens the panel and moves through the rows, Enter or Space toggles the focused row, and Escape closes the panel. ArrowLeft with the caret at the start of the input, or Backspace on an empty input, moves focus into the pills, where ArrowLeft and ArrowRight move between them, Delete removes the focused pill, and ArrowRight past the last pill returns to the input. When the field is too narrow to show every pill, they collapse into a count bubble; a click or ArrowDown on the bubble opens a dropdown listing them. The arrow button at the right edge toggles the panel and keeps focus in the input.
The simplest way to populate items is a flat array of strings or numbers: the displayed label and the stored value are the same. When you need a separate stored value (for example a country code behind a country name), pass an array of objects and tell the widget which keys to use via labelField and valueField.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiDropdown('frameworks', { items: ['React', 'Angular', 'Vue', 'Lit', 'Svelte', 'Solid'], placeholder: 'Type to search...', label: 'Frameworks', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiDropdown", "path": "frameworks", "label": "Frameworks", "props": { "items": [ "React", "Angular", "Vue", "Lit", "Svelte", "Solid" ], "placeholder": "Type to search..." } } ]}Use these props to customize your Multi Dropdown widget.
| prop | type | description |
|---|---|---|
items | array | Required. Array of strings, numbers, or plain objects. See Using Objects. |
autocomplete | string | 'on', 'off', or a space-separated list of W3C autofill tokens |
height | number | Height of the dropdown list container |
hint | string | A description to display below the input |
icon | string | A css class to display an icon inside the field, to the left of the pills |
inputDebounce | number | Delay in milliseconds before triggering a search/filter. Defaults to 500 |
itemHeight | number | Height of each individual item in the list |
itemRenderer | string | Key of a registered renderer used to draw each item. See Item Renderers |
labelField | string | When items are objects, the key whose value is shown to the user. Defaults to label. |
placeholder | string | Text shown in the input when it is empty |
removeAriaLabel | string | Localizable | Accessible description of each pill’s remove action. Defaults to Remove option |
removeIcon | string | CSS class string for a custom remove icon. When omitted, a built-in cross is rendered |
searchFields | array | List of fields to search on when filtering the list. Defaults to labelField |
toggleAriaLabel | string | Localizable | Aria label for the panel toggle button. Defaults to Show options |
valueField | string | When items are objects, the key whose value is stored in the form. Defaults to value. Required for selection to emit a primitive. |
inputDebounce, autocomplete and searchFields behave exactly as they do on the Dropdown. There is no prop that caps the number of selections; use the array validator’s maxItems instead, see Validation.
Using Objects
Section titled “Using Objects”The top-of-page demo uses an array of strings: each entry is both what the user sees and what gets stored. That falls short when you need a stable id behind a human-readable label.
For that case, pass items as plain objects and use labelField and valueField to tell the widget which key holds the displayed text and which one holds the stored value. The example pairs each country name with a short code, and searchFields lets the user filter by either. The pills show the label; the value array holds the codes.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiDropdown('countries', { placeholder: 'Search by name or code', labelField: 'name', valueField: 'code', searchFields: ['name', 'code'], items: [ { name: 'USA', code: 'US', }, { name: 'Canada', code: 'CA', }, { name: 'Mexico', code: 'MX', }, { name: 'Brazil', code: 'BR', }, { name: 'Argentina', code: 'AR', }, ], label: 'Countries', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiDropdown", "path": "countries", "label": "Countries", "props": { "placeholder": "Search by name or code", "labelField": "name", "valueField": "code", "searchFields": [ "name", "code" ], "items": [ { "name": "USA", "code": "US" }, { "name": "Canada", "code": "CA" }, { "name": "Mexico", "code": "MX" }, { "name": "Brazil", "code": "BR" }, { "name": "Argentina", "code": "AR" } ] } } ]}If your objects already use the default key names (label and value), you can omit labelField and valueField entirely. Otherwise both should be set so selection can extract the right primitive.
Use the property hint to provide instructions for the user.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiDropdown('languages', { hint: 'The panel stays open while you toggle. Escape closes it.', placeholder: 'Search languages', items: ['English', 'Spanish', 'French', 'German', 'Portuguese', 'Japanese'], label: 'Languages', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiDropdown", "path": "languages", "label": "Languages", "props": { "hint": "The panel stays open while you toggle. Escape closes it.", "placeholder": "Search languages", "items": [ "English", "Spanish", "French", "German", "Portuguese", "Japanese" ] } } ]}Use the property icon to add a visual indicator inside the field, to the left of the pills and the filter input. The value is a CSS class string, the same convention Select and Textinput use.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiDropdown('skills', { icon: 'checklist', placeholder: 'Search skills', items: ['TypeScript', 'CSS', 'Accessibility', 'Testing', 'Design systems'], label: 'Skills', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiDropdown", "path": "skills", "label": "Skills", "props": { "icon": "checklist", "placeholder": "Search skills", "items": [ "TypeScript", "CSS", "Accessibility", "Testing", "Design systems" ] } } ]}Item Renderer
Section titled “Item Renderer”Use the property itemRenderer to draw each row with a registered renderer instead of the default template. The key must match one of the renderers registered on the form host; see Item Renderers for the full setup.
The default template draws the check square; a custom renderer replaces the whole row, so read selected from the render context to mark toggled rows. Pills are not affected: they always show the item’s labelField text. This demo uses the registered countryItemRenderer to display each country with its flag.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiDropdown('destinations', { placeholder: 'Search countries', labelField: 'label', valueField: 'id', searchFields: ['label'], itemRenderer: 'countryItemRenderer', items: [ { id: 'AU', flag: '🇦🇺', label: 'Australia', }, { id: 'BR', flag: '🇧🇷', label: 'Brazil', }, { id: 'CA', flag: '🇨🇦', label: 'Canada', }, { id: 'FR', flag: '🇫🇷', label: 'France', }, { id: 'DE', flag: '🇩🇪', label: 'Germany', }, { id: 'JP', flag: '🇯🇵', label: 'Japan', }, { id: 'ES', flag: '🇪🇸', label: 'Spain', }, { id: 'US', flag: '🇺🇸', label: 'United States', }, { id: 'GB', flag: '🇬🇧', label: 'United Kingdom', }, ], label: 'Destinations', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiDropdown", "path": "destinations", "label": "Destinations", "props": { "placeholder": "Search countries", "labelField": "label", "valueField": "id", "searchFields": [ "label" ], "itemRenderer": "countryItemRenderer", "items": [ { "id": "AU", "flag": "🇦🇺", "label": "Australia" }, { "id": "BR", "flag": "🇧🇷", "label": "Brazil" }, { "id": "CA", "flag": "🇨🇦", "label": "Canada" }, { "id": "FR", "flag": "🇫🇷", "label": "France" }, { "id": "DE", "flag": "🇩🇪", "label": "Germany" }, { "id": "JP", "flag": "🇯🇵", "label": "Japan" }, { "id": "ES", "flag": "🇪🇸", "label": "Spain" }, { "id": "US", "flag": "🇺🇸", "label": "United States" }, { "id": "GB", "flag": "🇬🇧", "label": "United Kingdom" } ] } } ]}Use height to set the size of the panel’s scrollable list, and itemHeight to fix the row height used by the virtual scroller. Tune itemHeight whenever you use a custom itemRenderer whose rows are taller than the default.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiDropdown('cities', { hint: 'Open the panel to see the shorter list with compact rows', placeholder: 'Search cities', height: 120, itemHeight: 32, items: ['Amsterdam', 'Berlin', 'Buenos Aires', 'Lisbon', 'London', 'Madrid', 'Mexico City', 'New York', 'Paris', 'Rome', 'Tokyo', 'Toronto'], label: 'Cities', }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiDropdown", "path": "cities", "label": "Cities", "props": { "hint": "Open the panel to see the shorter list with compact rows", "placeholder": "Search cities", "height": 120, "itemHeight": 32, "items": [ "Amsterdam", "Berlin", "Buenos Aires", "Lisbon", "London", "Madrid", "Mexico City", "New York", "Paris", "Rome", "Tokyo", "Toronto" ] } } ]}Validation
Section titled “Validation”The value is an array, so the widget takes the array validator: required, minItems and maxItems. The widget never refuses a toggle. A selection past maxItems is stored and the validator flags it, so the user is told why instead of clicking an option that does nothing. Errors paint the field border, appear below the field, and are repeated inside the open panel so the panel never covers them. Override the default error copy per rule with a messages object; see Features / Validators for the full rule list.
The demo caps the selection at three. Toggle a fourth option and tab away to see the error.
import { gui } from '@golemui/gui-shared';
export default [ gui.inputs.multiDropdown('topics', { hint: 'A fourth selection is stored and flagged, not blocked', placeholder: 'Search topics', items: ['Forms', 'Validation', 'Accessibility', 'Theming', 'Schemas', 'MCP', 'Widget sets'], label: 'Topics (up to three)', validator: { required: true, maxItems: 3, messages: { required: 'Pick at least one topic', maxItems: 'No more than three topics', }, }, }),];{ "$schema": "https://golemui.com/schemas/form.schema.json", "form": [ { "kind": "input", "type": "multiDropdown", "path": "topics", "label": "Topics (up to three)", "props": { "hint": "A fourth selection is stored and flagged, not blocked", "placeholder": "Search topics", "items": [ "Forms", "Validation", "Accessibility", "Theming", "Schemas", "MCP", "Widget sets" ] }, "validator": { "type": "array", "required": true, "maxItems": 3, "messages": { "required": "Pick at least one topic", "maxItems": "No more than three topics" } } } ]}Styling
Section titled “Styling”Multi Dropdowns can be styled as explained in the Styling Guide. The field is a pills strip plus a combobox input; the panel reuses the Multi List styles.
CSS Variables
Section titled “CSS Variables”Following you will find a list with the CSS Variables and a quick description of what you will style.
| CSS Variable | Description |
|---|---|
--gui-border-default | Field border, divider after the icon and after the pills, panel border |
--gui-border-focus | Field border color when the input, a pill or the count bubble is focused |
--gui-shadow-focus | Field box shadow when the input, a pill or the count bubble is focused |
--gui-intent-error | Field and panel border color when invalid |
--gui-shadow-focus-error | Field box shadow when invalid and focused |
--gui-text-default | Input text and toggle arrow color |
--gui-font-sm | Input font size |
--gui-space-2 | Inset of the pills and the panel inside the field |
--gui-space-3 | Inset of the toggle arrow from the field edge |
--gui-intent-primary | Pill, count bubble and selected row background color |
--gui-color-white | Pill text and remove icon color |
--gui-radius-md | Pill, panel and check square border radius |
--gui-radius-full | Count bubble border radius |
--gui-pill-height | Pill height. 24px, 32px under pointer: coarse |
--gui-pill-action-size | Remove icon size. 16px, 24px under pointer: coarse |
--gui-pill-action-hit | Remove icon hit area. 24px, 32px under pointer: coarse |
--gui-pill-font-size | Pill label font size. var(--gui-font-xs), var(--gui-font-sm) under pointer: coarse |
Anatomy
Section titled “Anatomy”This is the anatomy of the Multi Dropdown Widget in case you want to use your CSS styles.
<gui-multi-dropdown-input class="gui-multi-dropdown gui-field"> <gui-label> <span class="gui-label" id="fieldUid_label"><span class="gui-label__text">Frameworks</span></span> </gui-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" tabindex="-1" aria-label="Selected options"> <span class="gui-sentinel gui-sentinel__start"></span> <button type="button" class="gui-pills__pill" tabindex="-1" aria-label="React" aria-description="Remove option"> <span class="gui-pills__pill-text">React</span> <span class="gui-pills__pill-remove" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true" > <path d="M165.66,101.66,139…"></path> </svg> </span> </button> <span class="gui-sentinel gui-sentinel__end"></span> </div> </div> <div class="gui-pills__compact"> <button type="button" aria-haspopup="true" class="gui-pills__count" aria-label="1 selected" aria-expanded="false" aria-controls="fieldUid_pills_dropdown" > 1 </button> </div> </gui-pills> <input type="text" role="combobox" class="gui-multi-select__input" aria-autocomplete="list" id="fieldUid" placeholder="Type to search..." aria-expanded="true" aria-controls="fieldUid-list" aria-labelledby="fieldUid_label" /> </div> </gui-multi-select-trigger> <button type="button" class="gui-dropdown__arrow" aria-haspopup="listbox" aria-label="Show options" aria-expanded="true" aria-controls="fieldUid-list" > <span class="gui-caret" aria-hidden="true"></span> </button> <div class="gui-picker__panel"> <gui-multi-list id="fieldUid-list" role="listbox" tabindex="0" aria-multiselectable="true" aria-activedescendant="fieldUid-item-0"> <div role="option" tabindex="-1" class="gui-list__item-wrapper" id="fieldUid-item-0" style="height: 40px" aria-selected="true" aria-disabled="false" > <div role="option" class="gui-list__item gui-list__item-focused gui-list__item-selected" aria-selected="true" aria-disabled="false" > <span class="gui-list__item-check" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 256 256"> <path d="M229.66,77.66l-128…"></path> </svg> </span> React </div> </div> <div role="option" tabindex="-1" class="gui-list__item-wrapper" id="fieldUid-item-1" style="height: 40px" aria-selected="false" aria-disabled="false" > <div role="option" class="gui-list__item" aria-selected="false" aria-disabled="false"> <span class="gui-list__item-check" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 256 256"> <path d="M229.66,77.66l-128…"></path> </svg> </span> Angular </div> </div> <!-- More items --> </gui-multi-list> </div> </div></gui-multi-dropdown-input>