Skip to content
Components, switch product

Dropdown

<gui-dropdown> picks one item out of many. The user types to search, and the list under the field shows the items that match. The list renders only the items in view, so a dropdown of thousands stays fast, and it draws the options itself: give it the items and it shows their text, or pass renderItem to draw each option your way.

Code of the example above
DropdownBasic.tsx
import { useState } from 'react';
import { GuiDropdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const cities = [
'Amsterdam', 'Athens', 'Barcelona', 'Belgrade', 'Berlin', 'Bratislava',
'Brussels', 'Bucharest', 'Budapest', 'Copenhagen', 'Dublin', 'Edinburgh',
'Florence', 'Geneva', 'Hamburg', 'Helsinki', 'Istanbul', 'Kraków', 'Lisbon',
'Ljubljana', 'London', 'Lyon', 'Madrid', 'Milan', 'Munich', 'Naples',
'Oslo', 'Paris', 'Porto', 'Prague', 'Reykjavík', 'Riga', 'Rome', 'Seville',
'Sofia', 'Stockholm', 'Tallinn', 'Valencia', 'Vienna', 'Vilnius', 'Warsaw',
'Zagreb', 'Zurich',
];
export function DropdownBasic() {
const [status, setStatus] = useState('');
return (
<>
<GuiDropdown
id="city"
label="City"
placeholder="Type to search"
items={cities}
onGuiChange={(event) => {
const city = event.detail.value;
setStatus(city ? `Selected: ${city}` : 'No city selected');
}}
/>
<p id="city-status" role="status">
{status}
</p>
</>
);
}

The element’s entry point is @golemui/gui-components/dropdown, which registers <gui-dropdown> and the gui-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.

CityField.tsx
import { useState } from 'react';
import { GuiDropdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const cities = ['Lisbon', 'Madrid', 'Paris'];
export function CityField() {
const [city, setCity] = useState('');
return (
<GuiDropdown
label="City"
name="city"
items={cities}
value={city}
onGuiChange={(event) => setCity(event.detail.value ?? '')}
/>
);
}

In HTML, items can also be a JSON attribute, as in some of the examples below. gui-change reports the picked item’s value in event.detail.value, and null when the user clears the field.

Properties

Property Type Default Description
value OptionValue – The value of the selected item.
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
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 an item, or cleared the field. detail.value is the new value.
gui-change The user picked an item, or cleared the field. detail.value is the new value.
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.

items takes strings and numbers, which are both the text and the value of an item, or objects. For objects, label-field names the key of the text, label by default, and value-field the key of the value, value by default. The value of the dropdown is the picked item’s value, here the airport code, while the field shows its text.

The search looks in the label-field and value-field keys. search-fields names other keys to look in: here the user can find Lisbon by its city, its code or its airport’s name. An object with disabled: true shows in the list but can’t be picked.

Code of the example above
DropdownObjects.tsx
import { useState } from 'react';
import { GuiDropdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const airports = [
{ code: 'AMS', city: 'Amsterdam', name: 'Schiphol' },
{ code: 'BCN', city: 'Barcelona', name: 'El Prat' },
{ code: 'BER', city: 'Berlin', name: 'Brandenburg' },
{ code: 'CDG', city: 'Paris', name: 'Charles de Gaulle' },
{ code: 'FCO', city: 'Rome', name: 'Fiumicino' },
{ code: 'LHR', city: 'London', name: 'Heathrow' },
{ code: 'LIS', city: 'Lisbon', name: 'Humberto Delgado' },
{ code: 'MAD', city: 'Madrid', name: 'Barajas' },
{ code: 'OPO', city: 'Porto', name: 'Francisco Sá Carneiro' },
{ code: 'ORY', city: 'Paris', name: 'Orly' },
];
const searchFields = ['city', 'code', 'name'];
export function DropdownObjects() {
const [status, setStatus] = useState('');
return (
<>
<GuiDropdown
id="airport"
label="Departure airport"
hint="Search by city, code or airport name."
labelField="city"
valueField="code"
searchFields={searchFields}
items={airports}
// The value is the item's value-field: the airport code.
onGuiChange={(event) => setStatus(`Value: ${event.detail.value}`)}
/>
<p id="airport-status" role="status">
{status}
</p>
</>
);
}

By default the dropdown searches items itself, ignoring case. When the items live on your server, add remote-filter: the dropdown then shows items as you give them, and fires gui-filter with what the user typed in event.detail.query. Load the matching items and set items. When the search is cleared, after a pick or when the list closes, gui-filter fires again with an empty query.

gui-filter waits until the user stops typing for input-debounce milliseconds, 500 by default, so a fast typist doesn’t send a request per key. It fires without remote-filter too, if you want to follow the search.

Code of the example above
DropdownRemoteFilter.tsx
import { useRef, useState } from 'react';
import { GuiDropdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
// Stands in for your API: it answers after a short delay.
const customers = [
'Ada Lovelace', 'Alan Turing', 'Barbara Liskov', 'Claude Shannon',
'Donald Knuth', 'Edsger Dijkstra', 'Frances Allen', 'Grace Hopper',
'Hedy Lamarr', 'John von Neumann', 'Katherine Johnson', 'Linus Torvalds',
'Margaret Hamilton', 'Radia Perlman', 'Tim Berners-Lee',
];
const searchCustomers = (query: string) =>
new Promise<string[]>((resolve) =>
setTimeout(() => {
const text = query.toLowerCase();
resolve(customers.filter((name) => name.toLowerCase().includes(text)));
}, 400),
);
export function DropdownRemoteFilter() {
const [items, setItems] = useState(customers);
const [status, setStatus] = useState('');
const latest = useRef('');
// With remote-filter, the dropdown shows `items` as they are:
// load the matches yourself.
const onFilter = async (query: string) => {
latest.current = query;
setStatus(query ? `Searching for "${query}"…` : '');
const matches = await searchCustomers(query);
// An answer to an older search arrived late: keep the newer one.
if (query !== latest.current) return;
setItems(matches);
setStatus(query ? `${matches.length} found` : '');
};
return (
<>
<GuiDropdown
id="customer"
label="Customer"
placeholder="Type a name"
inputDebounce={300}
remoteFilter
items={items}
onGuiFilter={(event) => onFilter(event.detail.query)}
/>
<p id="customer-status" role="status">
{status}
</p>
</>
);
}

The example keeps the last query, so an answer to an older search that arrives late doesn’t replace a newer one.

renderItem draws the content of each option. It gets the item, with your object in item.template, and the option’s state, and returns what to show:

dropdown.renderItem = (item, { html, selected, focused, disabled, index }) =>
html`<b>${item.template.symbol}</b> ${item.template.name}`;

html is the template tag of Lit, which GolemUI Components are built on, so you don’t import anything to use it, in any framework. It shows every value you put in ${} as text, never as markup, so the names that come from your server can’t inject HTML, and it works under a strict Content Security Policy. A string returned instead of a template shows as text too.

Code of the example above
DropdownRenderItem.tsx
import { GuiDropdown } from '@golemui/gui-components/react';
import type { GuiItemRenderer } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
type Currency = { code: string; name: string; symbol: string };
const currencies: Currency[] = [
{ code: 'AUD', name: 'Australian dollar', symbol: 'A$' },
{ code: 'BRL', name: 'Brazilian real', symbol: 'R$' },
{ code: 'CHF', name: 'Swiss franc', symbol: 'Fr' },
{ code: 'EUR', name: 'Euro', symbol: '€' },
{ code: 'GBP', name: 'Pound sterling', symbol: '£' },
{ code: 'INR', name: 'Indian rupee', symbol: '₹' },
{ code: 'JPY', name: 'Japanese yen', symbol: '¥' },
{ code: 'USD', name: 'US dollar', symbol: '$' },
];
const searchFields = ['name', 'code'];
// The option's content: `html` is Lit's, and it shows every value as text.
const renderCurrency: GuiItemRenderer<Currency> = (item, { html }) => html`
<span class="currency__symbol">${item.template.symbol}</span>
${item.template.name}
<span class="currency__code">${item.template.code}</span>
`;
const currencyStyles = `
#currency .gui-list__item-content {
display: flex;
align-items: center;
gap: var(--gui-space-2);
}
.currency__symbol {
min-width: 2ch;
font-weight: 600;
}
.currency__code {
margin-inline-start: auto;
font-size: var(--gui-font-xs);
opacity: 0.75;
}
`;
export function DropdownRenderItem() {
return (
<>
<GuiDropdown
id="currency"
label="Currency"
labelField="name"
valueField="code"
searchFields={searchFields}
value="EUR"
items={currencies}
renderItem={renderCurrency}
/>
<style>{currencyStyles}</style>
</>
);
}

The dropdown keeps the rest of each option: its id, role, aria-selected, height and click. The content goes inside a gui-list__item-content element, which fills the option, so the example lays it out as a flex row. To show a component of your framework in an option, see Item renderers.

The list renders the options in view and five more on each side, however many items there are. This dropdown has 10,000. height sets how tall the list grows before it scrolls, 300px by default, and item-height the height of each option, 40px by default: set it when your options are taller.

Code of the example above
DropdownLong.tsx
import { GuiDropdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
// 10,000 items: the list renders the ones in view and a few more.
const orders = Array.from({ length: 10000 }, (_, i) => `Order #${10001 + i}`);
export function DropdownLong() {
return (
<GuiDropdown
id="order"
label="Order"
placeholder="Type an order number"
items={orders}
/>
);
}

icon takes the class names of an icon and shows it at the start of the field. The icon is decorative and hidden from assistive technology. See Icons for the ways to draw it.

Code of the example above
DropdownIcon.tsx
import { GuiDropdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const offices = ['Berlin', 'Lisbon', 'Toronto'];
// Any icon font works: icon sets these classes on the icon element.
// This one draws a Material Icons ligature.
const iconStyles = `
.icon-place::before {
content: 'place';
font-family: 'Material Icons';
}
`;
export function DropdownIcon() {
return (
<>
<GuiDropdown label="Office" icon="icon-place" items={offices} />
<style>{iconStyles}</style>
</>
);
}

A disabled dropdown can’t be focused or opened, and a form doesn’t submit it. A readonly dropdown can be focused and opened to see the items, but the user can’t search or pick one, and a form submits its value.

Code of the example above
DropdownDisabledReadonly.tsx
import { GuiDropdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function DropdownDisabledReadonly() {
return (
<div className="gui-grid gui-grid--row">
<div className="gui-grid__cell">
<GuiDropdown
label="Currency"
value="EUR"
items={['EUR', 'USD', 'GBP']}
disabled
/>
</div>
<div className="gui-grid__cell">
<GuiDropdown
label="Workspace"
value="Acme"
items={['Acme', 'Globex']}
readOnly
/>
</div>
</div>
);
}

With a name, <gui-dropdown> submits the picked item’s value under that name, and nothing while none is picked. required blocks the submit until the user picks one. Submit the form below, then pick a country and submit again.

Code of the example above
DropdownNativeForm.tsx
import { useState, type SubmitEvent } from 'react';
import { GuiButton, GuiDropdown } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const countries = ['France', 'Germany', 'Italy', 'Portugal', 'Spain'];
export function DropdownNativeForm() {
const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(event.currentTarget));
setResult(`Submitted ${JSON.stringify(data)}`);
};
return (
<form id="shipping" onSubmit={onSubmit}>
<div className="gui-grid gui-grid--row">
<div className="gui-grid__cell gui-grid__cell--span-3">
<GuiDropdown
label="Ship to"
name="country"
placeholder="Type a country"
items={countries}
required
/>
</div>
<div className="gui-grid__cell">
<GuiButton type="submit" label="Continue" />
</div>
</div>
<p id="shipping-result" role="status">
{result}
</p>
</form>
);
}

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

<gui-dropdown> renders into the light DOM, so you can style every part with plain CSS. The list in the panel is a gui-list, which has a shadow root for its scrolling box; its options are in the light DOM. These are the parts of an open dropdown:

gui-dropdown
<gui-dropdown class="gui-dropdown gui-field">
<label class="gui-label" for="city" id="city_label">
<span class="gui-label__text">City<span aria-hidden="true"> *</span></span>
<div class="gui-widget-hint" id="city_hint" aria-hidden="true">
Where you live.
</div>
</label>
<div class="gui-widget">
<span
class="gui-widget-icon icon-place"
data-icon="icon-place"
aria-hidden="true"
></span>
<input
type="text"
role="combobox"
id="city"
class="gui-widget-input gui-dropdown--icon"
aria-autocomplete="list"
aria-expanded="true"
aria-controls="city-list"
aria-describedby="city_hint"
aria-invalid="true"
aria-errormessage="city_errors"
aria-required="true"
/>
<button
type="button"
class="gui-dropdown__arrow"
aria-label="Show options"
aria-haspopup="listbox"
aria-expanded="true"
aria-controls="city-list"
>
<span class="gui-caret" aria-hidden="true"></span>
</button>
<div class="gui-picker__panel">
<gui-list id="city-list" role="listbox" tabindex="0" aria-label="City">
<div
id="city-item-0"
role="option"
class="gui-list__item-wrapper"
aria-selected="false"
>
<div class="gui-list__item">
<div class="gui-list__item-content">Amsterdam</div>
</div>
</div>
<div
id="city-item-1"
role="option"
class="gui-list__item-wrapper"
aria-selected="true"
>
<div class="gui-list__item gui-list__item-selected">
<div class="gui-list__item-content">Berlin</div>
</div>
</div>
<!-- One option per item in view, and five more on each side. -->
</gui-list>
<div
class="gui-validator gui-validator--panel"
id="city_panel_errors"
aria-hidden="true"
>
<div class="gui-validator__error">Pick the city you live in.</div>
</div>
</div>
</div>
<div class="gui-validator" id="city_errors" role="alert">
<div class="gui-validator__error">Pick the city you live in.</div>
</div>
</gui-dropdown>
ClassPart
gui-dropdownThe element itself.
gui-fieldThe element itself. Lines its parts up with the fields next to it in a gui-grid row.
gui-labelThe label, with the text and the hint.
gui-label__textThe label text and the required marker.
gui-widget-hintThe hint.
gui-widgetThe box around the field, the button and the panel.
gui-widget-inputThe text field the user types in.
gui-dropdown--iconOn the text field when there is an icon: it makes room for it.
gui-widget-iconThe icon, with your icon classes.
gui-dropdown__arrowThe button that opens and closes the list. It turns while the list is open.
gui-caretThe arrow. See Carets.
gui-picker__panelThe floating panel under the field. hidden while the list is closed.
gui-list__item-wrapperAn option. With aria-disabled="true" it is greyed out and ignores the pointer.
gui-list__itemThe box of an option, with its padding and colors.
gui-list__item-selectedOn the box of the selected option.
gui-list__item-focusedOn the box of the active option.
gui-list__item-disabledOn the box of a disabled option.
gui-list__item-contentThe content of an option: the text, or what renderItem returns.
gui-validator--panelThe copy of the errors at the bottom of the open panel, hidden from screen readers.
gui-validatorThe errors container. It is always there, so the alert role announces new errors.
gui-validator__errorOne error message.

The ids derive from uid, or from a generated id such as gui-dropdown-3 when you don’t set one. Select by class or by tag, not by id. The options are only in the DOM while the list is open.

The dropdown reads these design tokens. Set them on :root to change every dropdown, or on any element to change the dropdowns inside it:

TokenWhat it styles
--gui-widget-sizeThe height of the field, and the icon’s box.
--gui-radius-mdThe corner radius of the field, the panel and the list.
--gui-bg-surfaceThe field’s background.
--gui-bg-defaultThe background of the panel and the list.
--gui-border-defaultThe border of the field, the panel and the list.
--gui-shadow-mdThe panel’s shadow.
--gui-space-2The panel’s padding, and the space between the list and its errors.
--gui-space-3The space between the arrow and the field’s end.
--gui-text-defaultThe text, the label, the arrow, the icon and the options.
--gui-font-familyThe font.
--gui-font-smThe text size of the label, the field and the options.
--gui-font-xsThe text size of the hint and the errors.
--gui-text-secondaryThe hint.
--gui-intent-primaryThe selected option’s background.
--gui-intent-primary-hoverThe background of the active option and the option under the pointer.
--gui-intent-primary-textThe text of the selected, active and hovered options.
--gui-border-focusThe border of the field and the list while focused.
--gui-shadow-focusThe focus ring, around the field, the list and the active option.
--gui-intent-errorThe border of the field and the panel, and the messages, on errors.
--gui-shadow-focus-errorThe focus ring when there are errors.
--gui-bg-disabledThe background when disabled, and of disabled options.
--gui-text-disabledThe text when disabled.

The arrow is a caret: --gui-caret-shape and --gui-caret-image change it, with every other caret. See Carets.

styles.css
/* Calmer selection colors in every dropdown. */
gui-dropdown {
--gui-intent-primary: #e8eefc;
--gui-intent-primary-hover: #d5e0fa;
--gui-intent-primary-text: #1d2b4f;
}

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

  • Combobox. The text field has the combobox role, reports whether the list is open with aria-expanded, and points at the list with aria-controls. The list is a listbox named by the label.
  • Name. The label names the field. Without one the field has no accessible name.
  • Description. The hint is the field’s description, through aria-describedby.
  • Toggle. The button at the end is named Show options, has aria-haspopup="listbox" and reports aria-expanded. Rename it with toggle-aria-label, or translate it for every element under the key showOptions.
  • Active option. In the list, the active option is announced through aria-activedescendant, so focus stays on the list while the arrow keys move.
  • Errors. While errors show, the field has aria-invalid="true" and points at them with aria-errormessage, and screen readers announce new errors as they appear. The copy in the panel is hidden from them, so they hear the errors once.
  • Required and read-only. required sets aria-required and readonly sets aria-readonly; the visual marker is hidden from screen readers.
KeyAction
TabMoves focus to the field, which opens the list, then to the button and the list, then out.
Typing in the fieldSearches the items, after input-debounce.
↓ in the fieldOpens the list and moves focus into it.
Enter in an empty fieldClears the value, and fires gui-change with null.
↓ ↑ in the listMoves to the next or previous option, skipping disabled ones.
Page Down Page UpMoves by the number of options that fit in the list.
Home EndMoves to the first or last option that isn’t disabled.
Enter SpacePicks the active option, closes the list and returns focus to the field.
EscapeCloses the list and returns focus to the field.

While the list is open, Escape doesn’t reach the page, so a dialog around the dropdown stays open. With the list closed, it does.

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