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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiDropdownEvents, GuiEventMap } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';
type DropdownEvent<K extends keyof typeof GuiDropdownEvents> = GuiEventMap< typeof GuiDropdownEvents>[K];
@Component({ selector: 'app-dropdown-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-dropdown id="city" label="City" placeholder="Type to search" [items]="cities" (gui-change)="onChange($event)" ></gui-dropdown> <p id="city-status" role="status">{{ status() }}</p> `,})export class DropdownBasicComponent { 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', ];
status = signal('');
onChange(event: Event) { const city = (event as DropdownEvent<'gui-change'>).detail.value; this.status.set(city ? `Selected: ${city}` : 'No city selected'); }}<script setup lang="ts">import { ref } from 'vue';import type { OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';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',];
const status = ref('');
function onChange(city: OptionValue | null | undefined) { status.value = city ? `Selected: ${city}` : 'No city selected';}</script>
<template> <gui-dropdown id="city" label="City" placeholder="Type to search" :items.prop="cities" @gui-change="onChange($event.detail.value)" ></gui-dropdown> <p id="city-status" role="status">{{ status }}</p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiDropdownEvents, GuiEventMap, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
type DropdownEvent<K extends keyof typeof GuiDropdownEvents> = GuiEventMap< typeof GuiDropdownEvents>[K];
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',];
@customElement('dropdown-basic')export class DropdownBasic extends LitElement { @state() private city?: OptionValue | null;
protected override createRenderRoot() { return this; }
private onChange(event: DropdownEvent<'gui-change'>) { this.city = event.detail.value ?? null; }
override render() { return html` <gui-dropdown id="city" label="City" placeholder="Type to search" .items=${cities} @gui-change=${this.onChange} ></gui-dropdown> <p id="city-status" role="status"> ${this.city === undefined ? '' : this.city ? `Selected: ${this.city}` : 'No city selected'} </p> `; }}<gui-dropdown id="city" label="City" placeholder="Type to search"></gui-dropdown><p id="city-status" role="status"></p>
<script type="module"> const dropdown = document.querySelector('#city');
dropdown.items = [ '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', ];
dropdown.addEventListener('gui-change', (event) => { const city = event.detail.value; document.querySelector('#city-status').textContent = city ? `Selected: ${city}` : 'No city selected'; });</script>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.
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 ?? '')} /> );}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, GuiDropdownEvents } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';
type ChangeEvent = GuiEventMap<typeof GuiDropdownEvents>['gui-change'];
@Component({ selector: 'app-city-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-dropdown label="City" name="city" [items]="cities" [value]="city" (gui-change)="onChange($event)" ></gui-dropdown> `,})export class CityFieldComponent { cities = ['Lisbon', 'Madrid', 'Paris'];
city = '';
onChange(event: Event) { this.city = (event as ChangeEvent).detail.value ?? ''; }}"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/dropdown';import '@golemui/gui-components/index.css';
const cities = ['Lisbon', 'Madrid', 'Paris'];
const city = ref('');</script>
<template> <gui-dropdown label="City" name="city" :items.prop="cities" :value="city" @gui-change="city = $event.detail.value ?? ''" /></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/dropdown';import '@golemui/gui-components/index.css';
const cities = ['Lisbon', 'Madrid', 'Paris'];
@customElement('city-field')export class CityField extends LitElement { @state() private city = '';
protected override createRenderRoot() { return this; }
override render() { return html` <gui-dropdown label="City" name="city" .items=${cities} .value=${this.city} @gui-change=${(event: CustomEvent) => (this.city = event.detail.value)} ></gui-dropdown> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-dropdown label="City" name="city"></gui-dropdown>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-dropdown');element.items = ['Lisbon', 'Madrid', 'Paris'];
element.addEventListener('gui-change', (event) => { console.log(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. |
Examples
Section titled “Examples”Items as objects
Section titled “Items as objects”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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiDropdownEvents, GuiEventMap } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';
type DropdownEvent<K extends keyof typeof GuiDropdownEvents> = GuiEventMap< typeof GuiDropdownEvents>[K];
@Component({ selector: 'app-dropdown-objects', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-dropdown id="airport" label="Departure airport" hint="Search by city, code or airport name." label-field="city" value-field="code" [searchFields]="searchFields" [items]="airports" (gui-change)="onChange($event)" ></gui-dropdown> <p id="airport-status" role="status">{{ status() }}</p> `,})export class DropdownObjectsComponent { searchFields = ['city', 'code', 'name']; 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' }, ];
status = signal('');
onChange(event: Event) { // The value is the item's value-field: the airport code. const value = (event as DropdownEvent<'gui-change'>).detail.value; this.status.set(`Value: ${value}`); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/dropdown';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'];
const status = ref('');</script>
<template> <!-- The value is the item's value-field: the airport code. --> <gui-dropdown id="airport" label="Departure airport" hint="Search by city, code or airport name." label-field="city" value-field="code" :searchFields.prop="searchFields" :items.prop="airports" @gui-change="status = `Value: ${$event.detail.value}`" ></gui-dropdown> <p id="airport-status" role="status">{{ status }}</p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiDropdownEvents, GuiEventMap, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
type DropdownEvent<K extends keyof typeof GuiDropdownEvents> = GuiEventMap< typeof GuiDropdownEvents>[K];
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'];
@customElement('dropdown-objects')export class DropdownObjects extends LitElement { @state() private airport?: OptionValue | null;
protected override createRenderRoot() { return this; }
private onChange(event: DropdownEvent<'gui-change'>) { // The value is the item's value-field: the airport code. this.airport = event.detail.value ?? null; }
override render() { return html` <gui-dropdown id="airport" label="Departure airport" hint="Search by city, code or airport name." label-field="city" value-field="code" .searchFields=${searchFields} .items=${airports} @gui-change=${this.onChange} ></gui-dropdown> <p id="airport-status" role="status"> ${this.airport === undefined ? '' : `Value: ${this.airport}`} </p> `; }}<gui-dropdown id="airport" label="Departure airport" hint="Search by city, code or airport name." label-field="city" value-field="code" search-fields='["city", "code", "name"]'></gui-dropdown><p id="airport-status" role="status"></p>
<script type="module"> const dropdown = document.querySelector('#airport');
dropdown.items = [ { 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' }, ];
dropdown.addEventListener('gui-change', (event) => { // The value is the item's value-field: the airport code. document.querySelector('#airport-status').textContent = `Value: ${event.detail.value}`; });</script>Searching on your server
Section titled “Searching on your server”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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiDropdownEvents, GuiEventMap } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';
type DropdownEvent<K extends keyof typeof GuiDropdownEvents> = GuiEventMap< typeof GuiDropdownEvents>[K];
// 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), );
@Component({ selector: 'app-dropdown-remote-filter', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-dropdown id="customer" label="Customer" placeholder="Type a name" input-debounce="300" remote-filter [items]="items()" (gui-filter)="onFilter($event)" ></gui-dropdown> <p id="customer-status" role="status">{{ status() }}</p> `,})export class DropdownRemoteFilterComponent { items = signal(customers); status = signal(''); latest = '';
// With remote-filter, the dropdown shows `items` as they are: load the matches yourself. async onFilter(event: Event) { const { query } = (event as DropdownEvent<'gui-filter'>).detail; this.latest = query; this.status.set(query ? `Searching for "${query}"…` : ''); const matches = await searchCustomers(query); // An answer to an older search arrived late: keep the newer one. if (query !== this.latest) return; this.items.set(matches); this.status.set(query ? `${matches.length} found` : ''); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/dropdown';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), );
const items = ref(customers);const status = ref('');
// With remote-filter, the dropdown shows `items` as they are: load the matches yourself.let latest = '';async function onFilter(query: string) { latest = query; status.value = query ? `Searching for "${query}"…` : ''; const matches = await searchCustomers(query); // An answer to an older search arrived late: keep the newer one. if (query !== latest) return; items.value = matches; status.value = query ? `${matches.length} found` : '';}</script>
<template> <gui-dropdown id="customer" label="Customer" placeholder="Type a name" :inputDebounce="300" :remoteFilter="true" :items.prop="items" @gui-filter="onFilter($event.detail.query)" ></gui-dropdown> <p id="customer-status" role="status">{{ status }}</p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiDropdownEvents, GuiEventMap } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
type DropdownEvent<K extends keyof typeof GuiDropdownEvents> = GuiEventMap< typeof GuiDropdownEvents>[K];
// 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), );
@customElement('dropdown-remote-filter')export class DropdownRemoteFilter extends LitElement { @state() private items = customers; @state() private status = ''; private latest = '';
protected override createRenderRoot() { return this; }
// With remote-filter, the dropdown shows `items` as they are: load the // matches yourself. private async onFilter(event: DropdownEvent<'gui-filter'>) { const { query } = event.detail; this.latest = query; this.status = query ? `Searching for "${query}"…` : ''; const matches = await searchCustomers(query); // An answer to an older search arrived late: keep the newer one. if (query !== this.latest) return; this.items = matches; this.status = query ? `${matches.length} found` : ''; }
override render() { return html` <gui-dropdown id="customer" label="Customer" placeholder="Type a name" input-debounce="300" remote-filter .items=${this.items} @gui-filter=${this.onFilter} ></gui-dropdown> <p id="customer-status" role="status">${this.status}</p> `; }}<gui-dropdown id="customer" label="Customer" placeholder="Type a name" input-debounce="300" remote-filter></gui-dropdown><p id="customer-status" role="status"></p>
<script type="module"> const dropdown = document.querySelector('#customer'); const status = document.querySelector('#customer-status');
// 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) => new Promise((resolve) => setTimeout(() => { const text = query.toLowerCase(); resolve(customers.filter((name) => name.toLowerCase().includes(text))); }, 400), );
dropdown.items = customers;
// With remote-filter, the dropdown shows `items` as they are: load the matches yourself. let latest = ''; dropdown.addEventListener('gui-filter', async (event) => { const { query } = event.detail; latest = query; status.textContent = query ? `Searching for "${query}"…` : ''; const matches = await searchCustomers(query); // An answer to an older search arrived late: keep the newer one. if (query !== latest) return; dropdown.items = matches; status.textContent = query ? `${matches.length} found` : ''; });</script>The example keeps the last query, so an answer to an older search that arrives late doesn’t replace a newer one.
Custom options
Section titled “Custom options”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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation,} from '@angular/core';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';
type Currency = { code: string; name: string; symbol: string };
@Component({ selector: 'app-dropdown-render-item', schemas: [CUSTOM_ELEMENTS_SCHEMA], // The element renders the options itself: the styles must not be scoped. encapsulation: ViewEncapsulation.None, template: ` <gui-dropdown id="currency" label="Currency" label-field="name" value-field="code" [searchFields]="searchFields" value="EUR" [items]="currencies" [renderItem]="renderCurrency" ></gui-dropdown> `, styles: ` #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 class DropdownRenderItemComponent { searchFields = ['name', 'code']; 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: '$' }, ];
// The option's content: `html` is Lit's, and it shows every value as text. 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> `;}<script setup lang="ts">import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';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>`;</script>
<template> <gui-dropdown id="currency" label="Currency" label-field="name" value-field="code" value="EUR" :searchFields.prop="searchFields" :items.prop="currencies" :renderItem.prop="renderCurrency" ></gui-dropdown></template>
<style>#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;}</style>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiItemRenderer } from '@golemui/gui-components';import '@golemui/gui-components/dropdown';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'];
@customElement('dropdown-render-item')export class DropdownRenderItem extends LitElement { protected override createRenderRoot() { return this; }
// The option's content: `html` is Lit's, and it shows every value as text. private 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> `;
override render() { return html` <gui-dropdown id="currency" label="Currency" label-field="name" value-field="code" value="EUR" .searchFields=${searchFields} .items=${currencies} .renderItem=${this.renderCurrency} ></gui-dropdown>
<style> #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; } </style> `; }}<gui-dropdown id="currency" label="Currency" label-field="name" value-field="code" search-fields='["name", "code"]' value="EUR"></gui-dropdown>
<script type="module"> const dropdown = document.querySelector('#currency');
dropdown.items = [ { 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: '$' }, ];
// The option's content: `html` is Lit's, and it shows every value as text. dropdown.renderItem = (item, { html }) => html` <span class="currency__symbol">${item.template.symbol}</span> ${item.template.name} <span class="currency__code">${item.template.code}</span> `;</script>
<style> #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; }</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.
Long lists
Section titled “Long lists”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.
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} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/dropdown';
@Component({ selector: 'app-dropdown-long', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-dropdown id="order" label="Order" placeholder="Type an order number" [items]="orders" ></gui-dropdown> `,})export class DropdownLongComponent { // 10,000 items: the list renders the ones in view and a few more. orders = Array.from({ length: 10000 }, (_, i) => `Order #${10001 + i}`);}<script setup lang="ts">import '@golemui/gui-components/dropdown';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}`);</script>
<template> <gui-dropdown id="order" label="Order" placeholder="Type an order number" :items.prop="orders" ></gui-dropdown></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/dropdown';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}`);
@customElement('dropdown-long')export class DropdownLong extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-dropdown id="order" label="Order" placeholder="Type an order number" .items=${orders} ></gui-dropdown> `; }}<gui-dropdown id="order" label="Order" placeholder="Type an order number"></gui-dropdown>
<script type="module"> const dropdown = document.querySelector('#order');
// 10,000 items: the list renders the ones in view and a few more. dropdown.items = Array.from({ length: 10000 }, (_, i) => `Order #${10001 + i}`);</script>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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation,} from '@angular/core';import '@golemui/gui-components/dropdown';
@Component({ selector: 'app-dropdown-icon', schemas: [CUSTOM_ELEMENTS_SCHEMA], // The element renders the icon itself: the style must not be scoped. encapsulation: ViewEncapsulation.None, template: ` <gui-dropdown label="Office" icon="icon-place" [items]="offices" ></gui-dropdown> `, styles: ` /* Any icon font works: icon sets these classes on the icon element. This one draws a Material Icons ligature. */ .icon-place::before { content: 'place'; font-family: 'Material Icons'; } `,})export class DropdownIconComponent { offices = ['Berlin', 'Lisbon', 'Toronto'];}<script setup lang="ts">import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
const offices = ['Berlin', 'Lisbon', 'Toronto'];</script>
<template> <gui-dropdown label="Office" icon="icon-place" :items.prop="offices" ></gui-dropdown></template>
<style>/* Any icon font works: icon sets these classes on the icon element. This one draws a Material Icons ligature. */.icon-place::before { content: 'place'; font-family: 'Material Icons';}</style>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
@customElement('dropdown-icon')export class DropdownIcon extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-dropdown label="Office" icon="icon-place" .items=${['Berlin', 'Lisbon', 'Toronto']} ></gui-dropdown>
<style> /* Any icon font works: icon sets these classes on the icon element. This one draws a Material Icons ligature. */ .icon-place::before { content: 'place'; font-family: 'Material Icons'; } </style> `; }}<gui-dropdown label="Office" icon="icon-place" items='["Berlin", "Lisbon", "Toronto"]'></gui-dropdown>
<style> /* Any icon font works: icon sets these classes on the icon element. This one draws a Material Icons ligature. */ .icon-place::before { content: 'place'; font-family: 'Material Icons'; }</style>Disabled and read-only
Section titled “Disabled and read-only”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/dropdown';
@Component({ selector: 'app-dropdown-disabled-readonly', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-dropdown label="Currency" value="EUR" [items]="currencies" disabled ></gui-dropdown> </div> <div class="gui-grid__cell"> <gui-dropdown label="Workspace" value="Acme" [items]="workspaces" readonly ></gui-dropdown> </div> </div> `,})export class DropdownDisabledReadonlyComponent { currencies = ['EUR', 'USD', 'GBP']; workspaces = ['Acme', 'Globex'];}<script setup lang="ts">import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
const currencies = ['EUR', 'USD', 'GBP'];const workspaces = ['Acme', 'Globex'];</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-dropdown label="Currency" value="EUR" :items.prop="currencies" :disabled="true" ></gui-dropdown> </div> <div class="gui-grid__cell"> <gui-dropdown label="Workspace" value="Acme" :items.prop="workspaces" :readOnly="true" ></gui-dropdown> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
@customElement('dropdown-disabled-readonly')export class DropdownDisabledReadonly extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-dropdown label="Currency" value="EUR" .items=${['EUR', 'USD', 'GBP']} disabled ></gui-dropdown> </div> <div class="gui-grid__cell"> <gui-dropdown label="Workspace" value="Acme" .items=${['Acme', 'Globex']} readonly ></gui-dropdown> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-dropdown label="Currency" value="EUR" items='["EUR", "USD", "GBP"]' disabled></gui-dropdown> </div> <div class="gui-grid__cell"> <gui-dropdown label="Workspace" value="Acme" items='["Acme", "Globex"]' readonly></gui-dropdown> </div></div>In a form
Section titled “In a form”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import '@golemui/gui-components/button';import '@golemui/gui-components/dropdown';
@Component({ selector: 'app-dropdown-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="shipping" #form (submit)="onSubmit($event, form)"> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell gui-grid__cell--span-3"> <gui-dropdown label="Ship to" name="country" placeholder="Type a country" [items]="countries" required ></gui-dropdown> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Continue"></gui-button> </div> </div> <p id="shipping-result" role="status">{{ result() }}</p> </form> `,})export class DropdownNativeFormComponent { countries = ['France', 'Germany', 'Italy', 'Portugal', 'Spain']; result = signal('');
onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); this.result.set(`Submitted ${JSON.stringify(data)}`); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/button';import '@golemui/gui-components/dropdown';import '@golemui/gui-components/index.css';
const countries = ['France', 'Germany', 'Italy', 'Portugal', 'Spain'];
const result = ref('');
function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; const data = Object.fromEntries(new FormData(event.currentTarget)); result.value = `Submitted ${JSON.stringify(data)}`;}</script>
<template> <form id="shipping" @submit.prevent="onSubmit"> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell gui-grid__cell--span-3"> <gui-dropdown label="Ship to" name="country" placeholder="Type a country" :items.prop="countries" :required="true" ></gui-dropdown> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Continue"></gui-button> </div> </div> <p id="shipping-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/dropdown';import '@golemui/gui-components/index.css';
const countries = ['France', 'Germany', 'Italy', 'Portugal', 'Spain'];
@customElement('dropdown-native-form')export class DropdownNativeForm extends LitElement { @query('#shipping') private form!: HTMLFormElement; @state() private data?: Record<string, FormDataEntryValue>;
protected override createRenderRoot() { return this; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); this.data = Object.fromEntries(new FormData(this.form)); }
override render() { return html` <form id="shipping" @submit=${this.onSubmit}> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell gui-grid__cell--span-3"> <gui-dropdown label="Ship to" name="country" placeholder="Type a country" .items=${countries} required ></gui-dropdown> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Continue"></gui-button> </div> </div> <p id="shipping-result" role="status"> ${this.data && `Submitted ${JSON.stringify(this.data)}`} </p> </form> `; }}<form id="shipping"> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell gui-grid__cell--span-3"> <gui-dropdown label="Ship to" name="country" placeholder="Type a country" items='["France", "Germany", "Italy", "Portugal", "Spain"]' required ></gui-dropdown> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Continue"></gui-button> </div> </div> <p id="shipping-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#shipping');
form.addEventListener('submit', (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); document.querySelector('#shipping-result').textContent = `Submitted ${JSON.stringify(data)}`; });</script>How the elements submit, validate and reset is in Native forms.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<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 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>| Class | Part |
|---|---|
gui-dropdown | The element itself. |
gui-field | The element itself. Lines its parts up with the fields next to it in a gui-grid row. |
gui-label | The label, with the text and the hint. |
gui-label__text | The label text and the required marker. |
gui-widget-hint | The hint. |
gui-widget | The box around the field, the button and the panel. |
gui-widget-input | The text field the user types in. |
gui-dropdown--icon | On the text field when there is an icon: it makes room for it. |
gui-widget-icon | The icon, with your icon classes. |
gui-dropdown__arrow | The button that opens and closes the list. It turns while the list is open. |
gui-caret | The arrow. See Carets. |
gui-picker__panel | The floating panel under the field. hidden while the list is closed. |
gui-list__item-wrapper | An option. With aria-disabled="true" it is greyed out and ignores the pointer. |
gui-list__item | The box of an option, with its padding and colors. |
gui-list__item-selected | On the box of the selected option. |
gui-list__item-focused | On the box of the active option. |
gui-list__item-disabled | On the box of a disabled option. |
gui-list__item-content | The content of an option: the text, or what renderItem returns. |
gui-validator--panel | The copy of the errors at the bottom of the open panel, hidden from screen readers. |
gui-validator | The errors container. It is always there, so the alert role announces new errors. |
gui-validator__error | One 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.
Tokens
Section titled “Tokens”The dropdown reads these design tokens. Set them on :root to change every dropdown, or on any
element to change the dropdowns inside it:
| Token | What it styles |
|---|---|
--gui-widget-size | The height of the field, and the icon’s box. |
--gui-radius-md | The corner radius of the field, the panel and the list. |
--gui-bg-surface | The field’s background. |
--gui-bg-default | The background of the panel and the list. |
--gui-border-default | The border of the field, the panel and the list. |
--gui-shadow-md | The panel’s shadow. |
--gui-space-2 | The panel’s padding, and the space between the list and its errors. |
--gui-space-3 | The space between the arrow and the field’s end. |
--gui-text-default | The text, the label, the arrow, the icon and the options. |
--gui-font-family | The font. |
--gui-font-sm | The text size of the label, the field and the options. |
--gui-font-xs | The text size of the hint and the errors. |
--gui-text-secondary | The hint. |
--gui-intent-primary | The selected option’s background. |
--gui-intent-primary-hover | The background of the active option and the option under the pointer. |
--gui-intent-primary-text | The text of the selected, active and hovered options. |
--gui-border-focus | The border of the field and the list while focused. |
--gui-shadow-focus | The focus ring, around the field, the list and the active option. |
--gui-intent-error | The border of the field and the panel, and the messages, on errors. |
--gui-shadow-focus-error | The focus ring when there are errors. |
--gui-bg-disabled | The background when disabled, and of disabled options. |
--gui-text-disabled | The text when disabled. |
The arrow is a caret: --gui-caret-shape and --gui-caret-image change it, with every other
caret. See Carets.
/* 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.
Accessibility
Section titled “Accessibility”- Combobox. The text field has the
comboboxrole, reports whether the list is open witharia-expanded, and points at the list witharia-controls. The list is alistboxnamed by thelabel. - Name. The
labelnames the field. Without one the field has no accessible name. - Description. The
hintis the field’s description, througharia-describedby. - Toggle. The button at the end is named
Show options, hasaria-haspopup="listbox"and reportsaria-expanded. Rename it withtoggle-aria-label, or translate it for every element under the keyshowOptions. - 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 witharia-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.
requiredsetsaria-requiredandreadonlysetsaria-readonly; the visual marker is hidden from screen readers.
| Key | Action |
|---|---|
| Tab | Moves focus to the field, which opens the list, then to the button and the list, then out. |
| Typing in the field | Searches the items, after input-debounce. |
| ↓ in the field | Opens the list and moves focus into it. |
| Enter in an empty field | Clears the value, and fires gui-change with null. |
| ↓ ↑ in the list | Moves to the next or previous option, skipping disabled ones. |
| Page Down Page Up | Moves by the number of options that fit in the list. |
| Home End | Moves to the first or last option that isn’t disabled. |
| Enter Space | Picks the active option, closes the list and returns focus to the field. |
| Escape | Closes 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.