List
<gui-list> is a listbox to pick one item out of many. It handles the keyboard, the focus, the
selection and the scrolling, and it renders only the items in view, so a list of thousands stays
fast. It doesn’t draw the items: you render them, as its children, in your own markup.
For a field the user types in to search, with the list in a popup and the options drawn for you,
use gui-dropdown.
import { useState } from 'react';import { GuiLabel, GuiList } from '@golemui/gui-components/react';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';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 CityField() { const [city, setCity] = useState<OptionValue>(); const [visibleItems, setVisibleItems] = useState<GuiVisibleItem[]>([]);
return ( <div className="gui-field"> <GuiLabel uid="city" label="City" /> <div className="gui-widget"> <GuiList id="city" uid="city" label="City" height={200} items={cities} value={city} onGuiVisibleItemsChange={(event) => setVisibleItems(event.detail)} onGuiChange={(event) => setCity(event.detail.value)} > {/* The list draws no items: it reports the ones to render, and you add them as its children. One option per item, from its id and state. A click on it picks the item. */} {visibleItems.map((item) => { const classes = ['gui-list__item']; if (item.selected) classes.push('gui-list__item-selected'); if (item.focused) classes.push('gui-list__item-focused'); return ( <div key={item.value} id={item.id} role="option" aria-selected={item.selected} className="gui-list__item-wrapper" style={{ height: 40 }} > <div className={classes.join(' ')}> {String(item.template)} </div> </div> ); })} </GuiList> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap<typeof GuiListEvents>[K];
@Component({ selector: 'app-city-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-field"> <gui-label uid="city" label="City"></gui-label> <div class="gui-widget"> <gui-list id="city" uid="city" label="City" height="200" [items]="cities" [value]="city()" (gui-visible-items-change)="onVisibleItemsChange($event)" (gui-change)="onChange($event)" > <!-- One option per item, from its id and state. A click on it picks the item. --> @for (item of visibleItems(); track item.value) { <div [id]="item.id" role="option" [attr.aria-selected]="item.selected" class="gui-list__item-wrapper" [style.height.px]="40" > <div class="gui-list__item" [class.gui-list__item-selected]="item.selected" [class.gui-list__item-focused]="item.focused" > {{ item.template }} </div> </div> } </gui-list> </div> </div> `,})export class CityFieldComponent { 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', ];
city = signal<OptionValue | undefined>(undefined); visibleItems = signal<GuiVisibleItem[]>([]);
// The list draws no items: it reports the ones to render, and you render // them as its children. onVisibleItemsChange(event: Event) { this.visibleItems.set( (event as ListEvent<'gui-visible-items-change'>).detail, ); }
onChange(event: Event) { this.city.set((event as ListEvent<'gui-change'>).detail.value); }}<script setup lang="ts">import { ref } from 'vue';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';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 city = ref<OptionValue>();// The list draws no items: it reports the ones to render, and you add them as its children.const visibleItems = ref<GuiVisibleItem[]>([]);</script>
<template> <div class="gui-field"> <gui-label uid="city" label="City"></gui-label> <div class="gui-widget"> <gui-list id="city" uid="city" label="City" :height="200" :items.prop="cities" :value.prop="city" @gui-visible-items-change="visibleItems = $event.detail" @gui-change="city = $event.detail.value" > <!-- One option per item, from its id and state. A click on it picks the item. --> <div v-for="item in visibleItems" :key="item.value" :id="item.id" role="option" :aria-selected="item.selected" class="gui-list__item-wrapper" :style="{ height: '40px' }" > <div class="gui-list__item" :class="{ 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, }" > {{ item.template }} </div> </div> </gui-list> </div> </div></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import { classMap } from 'lit/directives/class-map.js';import { repeat } from 'lit/directives/repeat.js';import { styleMap } from 'lit/directives/style-map.js';import type { GuiEventMap, GuiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/index.css';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap<typeof GuiListEvents>[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('city-field')export class CityField extends LitElement { @state() private city?: OptionValue; @state() private visibleItems: GuiVisibleItem[] = [];
protected override createRenderRoot() { return this; }
// The list draws no items: it reports the ones to render, and you render // them as its children. private onVisibleItemsChange(event: ListEvent<'gui-visible-items-change'>) { this.visibleItems = event.detail; }
private onChange(event: ListEvent<'gui-change'>) { this.city = event.detail.value; }
override render() { return html` <div class="gui-field"> <gui-label uid="city" label="City"></gui-label> <div class="gui-widget"> <gui-list id="city" uid="city" label="City" height="200" .items=${cities} .value=${this.city} @gui-visible-items-change=${this.onVisibleItemsChange} @gui-change=${this.onChange} > ${repeat( this.visibleItems, (item) => item.value, // One option per item, from its id and state. A click on it // picks the item. (item) => html` <div id=${item.id} role="option" aria-selected=${item.selected} class="gui-list__item-wrapper" style=${styleMap({ height: '40px' })} > <div class=${classMap({ 'gui-list__item': true, 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, })} > ${item.template} </div> </div> `, )} </gui-list> </div> </div> `; }}<div class="gui-field"> <gui-label uid="city" label="City"></gui-label> <div class="gui-widget"> <gui-list id="city" uid="city" label="City" height="200"></gui-list> </div></div>
<script type="module"> const list = document.querySelector('#city'); // The list draws no items: it reports the ones to render, and you add them as its children. list.addEventListener('gui-visible-items-change', (event) => { list.querySelectorAll('[role="option"]').forEach((option) => option.remove()); list.prepend(...event.detail.map(renderOption)); });
// One option per item, from its id and state. A click on it picks the item. function renderOption(item) { const option = document.createElement('div'); option.id = item.id; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', String(item.selected)); option.className = 'gui-list__item-wrapper'; option.style.height = '40px'; const text = document.createElement('div'); text.className = 'gui-list__item'; text.classList.toggle('gui-list__item-selected', item.selected); text.classList.toggle('gui-list__item-focused', item.focused); text.textContent = item.template; option.append(text); return option; }
list.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', ];</script>The element’s entry point is @golemui/gui-components/list, which registers <gui-list> and
nothing else. The examples also use gui-label for a visible
label. 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 { GuiList } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
const cities = ['Lisbon', 'Porto', 'Madrid'];
export function CityField() { const [city, setCity] = useState('');
return ( <GuiList 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, GuiListEvents } from '@golemui/gui-components';import '@golemui/gui-components/list';
type ChangeEvent = GuiEventMap<typeof GuiListEvents>['gui-change'];
@Component({ selector: 'app-city-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-list label="City" name="city" [items]="cities" [value]="city" (gui-change)="onChange($event)" ></gui-list> `,})export class CityFieldComponent { cities = ['Lisbon', 'Porto', 'Madrid'];
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/list';import '@golemui/gui-components/index.css';
const cities = ['Lisbon', 'Porto', 'Madrid'];
const city = ref('');</script>
<template> <gui-list 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/list';import '@golemui/gui-components/index.css';
const cities = ['Lisbon', 'Porto', 'Madrid'];
@customElement('city-field')export class CityField extends LitElement { @state() private city = '';
protected override createRenderRoot() { return this; }
override render() { return html` <gui-list label="City" name="city" .items=${cities} .value=${this.city} @gui-change=${(event: CustomEvent) => (this.city = event.detail.value)} ></gui-list> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-list label="City" name="city"></gui-list>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/list';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-list');element.items = ['Lisbon', 'Porto', 'Madrid'];
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});Then render the items in range as its children, as below.
Rendering the items
Section titled “Rendering the items”The list holds the items, the value and the active item, the one the arrow keys move, and picks
an item on a click, Enter or Space. Your code renders the options, as the
list’s children. The list reports the items to render with gui-visible-items-change: only
the ones in view, each with its option’s id and its state, selected, focused and disabled.
Item renderers explains the items it reports and what each option needs, with this example in React, Angular, Vue, Lit and plain JavaScript.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
value | OptionValue | – | The value of the selected item. |
valueField
attribute value-field | string | – | For items given as objects, the key of the value. |
items | ListItemInput[] | [] | The items of the list: values, or objects whose value value-field names. The list normalizes them to { template, value } objects, the detail of gui-update-items. |
itemHeight
attribute item-height | number | – | Height of each item, in pixels. Needed to virtualize the list. |
height | number | – | Height of the scrollable list, in pixels. |
visibleItems read-only, no attribute | GuiVisibleItem[] | – | The items to render: the ones in view and five more on each side, each with its index, its option id and its state. Render one option per entry, as the list's children. |
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-visible-items-change | The items to render changed: the user scrolled, items changed, or an item was selected or became the active one. detail is the new visibleItems. |
gui-input | The user changed the value. detail.value is the new value. |
gui-change | The user committed the value. detail.value is the committed value. |
gui-blur | Focus left the control. |
gui-focus-change | The focused item changed. detail.index is its index, or -1. |
gui-range-change | The rendered items changed while scrolling. detail has the startIndex and endIndex. |
gui-update-items | The items were normalized. detail is the list of items. |
Examples
Section titled “Examples”Items as objects
Section titled “Items as objects”items takes strings or numbers, which are both the text and the value of an item, or objects.
value-field names the key of an object’s value, value by default. The object itself is the
item’s template, so you can show any of its keys.
An object with disabled: true is a disabled item: the arrow keys skip it and Enter
doesn’t pick it, and neither does a click. Its disabled is true in visibleItems: give its
option aria-disabled="true".
label and hint name and describe the listbox for screen readers, but the list doesn’t show
them. Show them with a gui-label of the same uid, above the
list; the list’s id is the same, so the native label focuses the list when clicked.
import { useState } from 'react';import { GuiLabel, GuiList } from '@golemui/gui-components/react';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
type Room = { id: string; name: string; seats: number; disabled?: boolean };
const rooms: Room[] = [ { id: 'lovelace', name: 'Lovelace', seats: 4 }, { id: 'hopper', name: 'Hopper', seats: 8, disabled: true }, { id: 'turing', name: 'Turing', seats: 6 }, { id: 'noether', name: 'Noether', seats: 12, disabled: true }, { id: 'curie', name: 'Curie', seats: 20 },];
export function ListObjects() { const [room, setRoom] = useState<OptionValue>(); const [visibleItems, setVisibleItems] = useState<GuiVisibleItem<Room>[]>( [], );
return ( <div className="gui-field"> <GuiLabel uid="room" label="Meeting room" hint="Rooms in use can't be booked." /> <div className="gui-widget"> <GuiList id="room" uid="room" label="Meeting room" hint="Rooms in use can't be booked." valueField="id" height={160} items={rooms} value={room} onGuiVisibleItemsChange={(event) => // The items are the rooms: the event types them as unknown. setVisibleItems(event.detail as GuiVisibleItem<Room>[]) } onGuiChange={(event) => setRoom(event.detail.value)} > {visibleItems.map((item) => { // `template` is your object: show any of its keys. const { name, seats } = item.template; const classes = ['gui-list__item']; if (item.selected) classes.push('gui-list__item-selected'); if (item.focused) classes.push('gui-list__item-focused'); if (item.disabled) classes.push('gui-list__item-disabled'); return ( <div key={item.value} id={item.id} role="option" aria-selected={item.selected} aria-disabled={item.disabled} className="gui-list__item-wrapper" style={{ height: 40 }} > <div className={classes.join(' ')} >{`${name}, ${seats} seats`}</div> </div> ); })} </GuiList> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap< typeof GuiListEvents>[K];
type Room = { id: string; name: string; seats: number; disabled?: boolean };
@Component({ selector: 'app-list-objects', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-field"> <gui-label uid="room" label="Meeting room" hint="Rooms in use can't be booked." ></gui-label> <div class="gui-widget"> <gui-list id="room" uid="room" label="Meeting room" hint="Rooms in use can't be booked." value-field="id" height="160" [items]="rooms" [value]="room()" (gui-visible-items-change)="onVisibleItemsChange($event)" (gui-change)="onChange($event)" > @for (item of visibleItems(); track item.value) { <div [id]="item.id" role="option" [attr.aria-selected]="item.selected" [attr.aria-disabled]="item.disabled" class="gui-list__item-wrapper" [style.height.px]="40" > <!-- template is your object: show any of its keys. --> <div class="gui-list__item" [class.gui-list__item-selected]="item.selected" [class.gui-list__item-focused]="item.focused" [class.gui-list__item-disabled]="item.disabled" > {{ item.template.name }}, {{ item.template.seats }} seats </div> </div> } </gui-list> </div> </div> `,})export class ListObjectsComponent { rooms: Room[] = [ { id: 'lovelace', name: 'Lovelace', seats: 4 }, { id: 'hopper', name: 'Hopper', seats: 8, disabled: true }, { id: 'turing', name: 'Turing', seats: 6 }, { id: 'noether', name: 'Noether', seats: 12, disabled: true }, { id: 'curie', name: 'Curie', seats: 20 }, ];
room = signal<OptionValue | undefined>(undefined); visibleItems = signal<GuiVisibleItem<Room>[]>([]);
onVisibleItemsChange(event: Event) { const { detail } = event as ListEvent<'gui-visible-items-change'>; this.visibleItems.set(detail as GuiVisibleItem<Room>[]); }
onChange(event: Event) { this.room.set((event as ListEvent<'gui-change'>).detail.value); }}<script setup lang="ts">import { ref } from 'vue';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/index.css';
type Room = { id: string; name: string; seats: number; disabled?: boolean };
const rooms: Room[] = [ { id: 'lovelace', name: 'Lovelace', seats: 4 }, { id: 'hopper', name: 'Hopper', seats: 8, disabled: true }, { id: 'turing', name: 'Turing', seats: 6 }, { id: 'noether', name: 'Noether', seats: 12, disabled: true }, { id: 'curie', name: 'Curie', seats: 20 },];
const room = ref<OptionValue>();// The items are the rooms: the event types them as unknown.const visibleItems = ref<GuiVisibleItem<Room>[]>([]);</script>
<template> <div class="gui-field"> <gui-label uid="room" label="Meeting room" hint="Rooms in use can't be booked." ></gui-label> <div class="gui-widget"> <gui-list id="room" uid="room" label="Meeting room" hint="Rooms in use can't be booked." value-field="id" :height="160" :items.prop="rooms" :value.prop="room" @gui-visible-items-change=" visibleItems = $event.detail as GuiVisibleItem<Room>[] " @gui-change="room = $event.detail.value" > <div v-for="item in visibleItems" :key="item.value" :id="item.id" role="option" :aria-selected="item.selected" :aria-disabled="item.disabled" class="gui-list__item-wrapper" :style="{ height: '40px' }" > <div class="gui-list__item" :class="{ 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, 'gui-list__item-disabled': item.disabled, }" > <!-- `template` is your object: show any of its keys. --> {{ item.template.name }}, {{ item.template.seats }} seats </div> </div> </gui-list> </div> </div></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import { classMap } from 'lit/directives/class-map.js';import { repeat } from 'lit/directives/repeat.js';import { styleMap } from 'lit/directives/style-map.js';import type { GuiEventMap, GuiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/index.css';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap< typeof GuiListEvents>[K];
type Room = { id: string; name: string; seats: number; disabled?: boolean };
const rooms: Room[] = [ { id: 'lovelace', name: 'Lovelace', seats: 4 }, { id: 'hopper', name: 'Hopper', seats: 8, disabled: true }, { id: 'turing', name: 'Turing', seats: 6 }, { id: 'noether', name: 'Noether', seats: 12, disabled: true }, { id: 'curie', name: 'Curie', seats: 20 },];
@customElement('list-objects')export class ListObjects extends LitElement { @state() private room?: OptionValue; @state() private visibleItems: GuiVisibleItem<Room>[] = [];
protected override createRenderRoot() { return this; }
private onVisibleItemsChange(event: ListEvent<'gui-visible-items-change'>) { // The items are the rooms: the event types them as unknown. this.visibleItems = event.detail as GuiVisibleItem<Room>[]; }
private onChange(event: ListEvent<'gui-change'>) { this.room = event.detail.value; }
override render() { return html` <div class="gui-field"> <gui-label uid="room" label="Meeting room" hint="Rooms in use can't be booked." ></gui-label> <div class="gui-widget"> <gui-list id="room" uid="room" label="Meeting room" hint="Rooms in use can't be booked." value-field="id" height="160" .items=${rooms} .value=${this.room} @gui-visible-items-change=${this.onVisibleItemsChange} @gui-change=${this.onChange} > ${repeat( this.visibleItems, (item) => item.value, (item) => { // `template` is your object: show any of its keys. const { name, seats } = item.template; return html` <div id=${item.id} role="option" aria-selected=${item.selected} aria-disabled=${item.disabled} class="gui-list__item-wrapper" style=${styleMap({ height: '40px' })} > <div class=${classMap({ 'gui-list__item': true, 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, 'gui-list__item-disabled': item.disabled, })} > ${name}, ${seats} seats </div> </div> `; }, )} </gui-list> </div> </div> `; }}<div class="gui-field"> <gui-label uid="room" label="Meeting room" hint="Rooms in use can't be booked."></gui-label> <div class="gui-widget"> <gui-list id="room" uid="room" label="Meeting room" hint="Rooms in use can't be booked." value-field="id" height="160" ></gui-list> </div></div>
<script type="module"> const list = document.querySelector('#room');
list.addEventListener('gui-visible-items-change', (event) => { list.querySelectorAll('[role="option"]').forEach((option) => option.remove()); list.prepend(...event.detail.map(renderOption)); });
function renderOption(item) { const option = document.createElement('div'); option.id = item.id; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', String(item.selected)); option.setAttribute('aria-disabled', String(item.disabled)); option.className = 'gui-list__item-wrapper'; option.style.height = '40px'; const text = document.createElement('div'); text.className = 'gui-list__item'; text.classList.toggle('gui-list__item-selected', item.selected); text.classList.toggle('gui-list__item-focused', item.focused); text.classList.toggle('gui-list__item-disabled', item.disabled); // `template` is your object: show any of its keys. text.textContent = `${item.template.name}, ${item.template.seats} seats`; option.append(text); return option; }
list.items = [ { id: 'lovelace', name: 'Lovelace', seats: 4 }, { id: 'hopper', name: 'Hopper', seats: 8, disabled: true }, { id: 'turing', name: 'Turing', seats: 6 }, { id: 'noether', name: 'Noether', seats: 12, disabled: true }, { id: 'curie', name: 'Curie', seats: 20 }, ];</script>Long lists and filtering
Section titled “Long lists and filtering”The list renders the items in view and five more on each side, whatever the length of items.
height is the most the list grows, 300px by default, before it scrolls, and item-height the
height of each item.
The list doesn’t filter its items. To filter them, as the user types in a search field, set
items to the ones that match. This list has 10,000 invoices:
import { useMemo, useState } from 'react';import { GuiLabel, GuiList, GuiTextinput } from '@golemui/gui-components/react';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const invoices = Array.from( { length: 10000 }, (_, i) => `INV-${String(i + 1).padStart(5, '0')}`,);
export function ListLong() { const [query, setQuery] = useState(''); const [invoice, setInvoice] = useState<OptionValue>(); const [visibleItems, setVisibleItems] = useState<GuiVisibleItem[]>([]);
// Filtering is yours: give the list the items that match. const matches = useMemo( () => invoices.filter((invoice) => invoice.includes(query)), [query], );
return ( <div className="gui-grid"> <div className="gui-grid__cell"> <GuiTextinput id="invoice-search" label="Search invoices" placeholder="For example, 0420" onGuiInput={(event) => setQuery((event.detail.value ?? '').trim().toUpperCase()) } /> </div> <div className="gui-grid__cell"> <div className="gui-field"> <GuiLabel uid="invoice" label="Invoice" /> <div className="gui-widget"> <GuiList id="invoice" uid="invoice" label="Invoice" height={192} itemHeight={32} items={matches} value={invoice} onGuiVisibleItemsChange={(event) => setVisibleItems(event.detail)} onGuiChange={(event) => setInvoice(event.detail.value)} > {visibleItems.map((item) => { const classes = ['gui-list__item']; if (item.selected) classes.push('gui-list__item-selected'); if (item.focused) classes.push('gui-list__item-focused'); return ( <div key={item.value} id={item.id} role="option" aria-selected={item.selected} className="gui-list__item-wrapper" // The same height as itemHeight. style={{ height: 32 }} > <div className={classes.join(' ')}> {String(item.template)} </div> </div> ); })} </GuiList> </div> </div> </div> </div> );}import { Component, computed, CUSTOM_ELEMENTS_SCHEMA, signal,} from '@angular/core';import type { GuiEventMap, GuiListEvents, GuiTextinputEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/textinput';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap< typeof GuiListEvents>[K];type SearchEvent = GuiEventMap<typeof GuiTextinputEvents>['gui-input'];
@Component({ selector: 'app-list-long', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-textinput id="invoice-search" label="Search invoices" placeholder="For example, 0420" (gui-input)="onSearch($event)" ></gui-textinput> </div> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="invoice" label="Invoice"></gui-label> <div class="gui-widget"> <gui-list id="invoice" uid="invoice" label="Invoice" height="192" item-height="32" [items]="matches()" [value]="invoice()" (gui-visible-items-change)="onVisibleItemsChange($event)" (gui-change)="onChange($event)" > @for (item of visibleItems(); track item.value) { <!-- The same height as item-height. --> <div [id]="item.id" role="option" [attr.aria-selected]="item.selected" class="gui-list__item-wrapper" [style.height.px]="32" > <div class="gui-list__item" [class.gui-list__item-selected]="item.selected" [class.gui-list__item-focused]="item.focused" > {{ item.template }} </div> </div> } </gui-list> </div> </div> </div> </div> `,})export class ListLongComponent { invoices = Array.from( { length: 10000 }, (_, i) => `INV-${String(i + 1).padStart(5, '0')}`, );
query = signal(''); invoice = signal<OptionValue | undefined>(undefined); visibleItems = signal<GuiVisibleItem[]>([]);
// Filtering is yours: give the list the items that match. matches = computed(() => this.invoices.filter((invoice) => invoice.includes(this.query())), );
onSearch(event: Event) { const { value } = (event as SearchEvent).detail; this.query.set((value ?? '').trim().toUpperCase()); }
onVisibleItemsChange(event: Event) { this.visibleItems.set( (event as ListEvent<'gui-visible-items-change'>).detail, ); }
onChange(event: Event) { this.invoice.set((event as ListEvent<'gui-change'>).detail.value); }}<script setup lang="ts">import { computed, ref } from 'vue';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/textinput';import '@golemui/gui-components/index.css';
const invoices = Array.from( { length: 10000 }, (_, i) => `INV-${String(i + 1).padStart(5, '0')}`,);
const query = ref('');const invoice = ref<OptionValue>();const visibleItems = ref<GuiVisibleItem[]>([]);
// Filtering is yours: give the list the items that match.const matches = computed(() => invoices.filter((invoice) => invoice.includes(query.value)),);</script>
<template> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-textinput id="invoice-search" label="Search invoices" placeholder="For example, 0420" @gui-input="query = ($event.detail.value ?? '').trim().toUpperCase()" ></gui-textinput> </div> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="invoice" label="Invoice"></gui-label> <div class="gui-widget"> <gui-list id="invoice" uid="invoice" label="Invoice" :height="192" :itemHeight="32" :items.prop="matches" :value.prop="invoice" @gui-visible-items-change="visibleItems = $event.detail" @gui-change="invoice = $event.detail.value" > <!-- The same height as item-height. --> <div v-for="item in visibleItems" :key="item.value" :id="item.id" role="option" :aria-selected="item.selected" class="gui-list__item-wrapper" :style="{ height: '32px' }" > <div class="gui-list__item" :class="{ 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, }" > {{ item.template }} </div> </div> </gui-list> </div> </div> </div> </div></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import { classMap } from 'lit/directives/class-map.js';import { repeat } from 'lit/directives/repeat.js';import { styleMap } from 'lit/directives/style-map.js';import type { GuiEventMap, GuiListEvents, GuiTextinputEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/textinput';import '@golemui/gui-components/index.css';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap< typeof GuiListEvents>[K];type TextinputEvent<K extends keyof typeof GuiTextinputEvents> = GuiEventMap< typeof GuiTextinputEvents>[K];
const invoices = Array.from( { length: 10000 }, (_, i) => `INV-${String(i + 1).padStart(5, '0')}`,);
@customElement('list-long')export class ListLong extends LitElement { @state() private invoice?: OptionValue; @state() private filtered = invoices; @state() private visibleItems: GuiVisibleItem[] = [];
protected override createRenderRoot() { return this; }
// Filtering is yours: give the list the items that match. private onSearch(event: TextinputEvent<'gui-input'>) { const query = (event.detail.value ?? '').trim().toUpperCase(); this.filtered = invoices.filter((invoice) => invoice.includes(query)); }
private onVisibleItemsChange(event: ListEvent<'gui-visible-items-change'>) { this.visibleItems = event.detail; }
private onChange(event: ListEvent<'gui-change'>) { this.invoice = event.detail.value; }
override render() { return html` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-textinput id="invoice-search" label="Search invoices" placeholder="For example, 0420" @gui-input=${this.onSearch} ></gui-textinput> </div> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="invoice" label="Invoice"></gui-label> <div class="gui-widget"> <gui-list id="invoice" uid="invoice" label="Invoice" height="192" item-height="32" .items=${this.filtered} .value=${this.invoice} @gui-visible-items-change=${this.onVisibleItemsChange} @gui-change=${this.onChange} > ${repeat( this.visibleItems, (item) => item.value, (item) => html` <div id=${item.id} role="option" aria-selected=${item.selected} class="gui-list__item-wrapper" style=${styleMap({ // The same height as item-height. height: '32px', })} > <div class=${classMap({ 'gui-list__item': true, 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, })} > ${item.template} </div> </div> `, )} </gui-list> </div> </div> </div> </div> `; }}<div class="gui-grid"> <div class="gui-grid__cell"> <gui-textinput id="invoice-search" label="Search invoices" placeholder="For example, 0420"></gui-textinput> </div> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="invoice" label="Invoice"></gui-label> <div class="gui-widget"> <gui-list id="invoice" uid="invoice" label="Invoice" height="192" item-height="32"></gui-list> </div> </div> </div></div>
<script type="module"> const list = document.querySelector('#invoice'); const search = document.querySelector('#invoice-search'); const invoices = Array.from({ length: 10000 }, (_, i) => `INV-${String(i + 1).padStart(5, '0')}`); list.addEventListener('gui-visible-items-change', (event) => { list.querySelectorAll('[role="option"]').forEach((option) => option.remove()); list.prepend(...event.detail.map(renderOption)); });
function renderOption(item) { const option = document.createElement('div'); option.id = item.id; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', String(item.selected)); option.className = 'gui-list__item-wrapper'; // The same height as item-height. option.style.height = '32px'; const text = document.createElement('div'); text.className = 'gui-list__item'; text.classList.toggle('gui-list__item-selected', item.selected); text.classList.toggle('gui-list__item-focused', item.focused); text.textContent = item.template; option.append(text); return option; }
// Filtering is yours: give the list the items that match. search.addEventListener('gui-input', (event) => { const query = (event.detail.value ?? '').trim().toUpperCase(); list.items = invoices.filter((invoice) => invoice.includes(query)); });
list.items = invoices;</script>Disabled and read-only
Section titled “Disabled and read-only”A disabled list leaves the tab order, and the keyboard does nothing. A readonly list can be
focused and browsed with the arrow keys, but Enter and Space don’t pick an
item, and a form submits its value. In either state a click picks nothing.
In a form
Section titled “In a form”With a name, <gui-list> submits the selected item’s value under that name, and nothing while
no item is selected. required blocks the submit until the user picks one. Submit the form below,
then pick a team and submit again.
import { useState, type SubmitEvent } from 'react';import { GuiButton, GuiLabel, GuiList } from '@golemui/gui-components/react';import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const teams = ['Design', 'Engineering', 'Research', 'Support'];
export function ListNativeForm() { const [team, setTeam] = useState<OptionValue>(); const [visibleItems, setVisibleItems] = useState<GuiVisibleItem[]>([]); 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="team-form" onSubmit={onSubmit}> <div className="gui-grid"> <div className="gui-grid__cell"> <div className="gui-field"> <GuiLabel uid="team" label="Team" required /> <div className="gui-widget"> <GuiList id="team" uid="team" label="Team" name="team" height={160} required items={teams} value={team} onGuiVisibleItemsChange={(event) => setVisibleItems(event.detail) } onGuiChange={(event) => setTeam(event.detail.value)} > {visibleItems.map((item) => { const classes = ['gui-list__item']; if (item.selected) classes.push('gui-list__item-selected'); if (item.focused) classes.push('gui-list__item-focused'); return ( <div key={item.value} id={item.id} role="option" aria-selected={item.selected} className="gui-list__item-wrapper" style={{ height: 40 }} > <div className={classes.join(' ')}> {String(item.template)} </div> </div> ); })} </GuiList> </div> </div> </div> <div className="gui-grid__cell"> <GuiButton type="submit" label="Join team" /> </div> </div> <p id="team-result" role="status"> {result} </p> </form> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/label';import '@golemui/gui-components/list';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap< typeof GuiListEvents>[K];
@Component({ selector: 'app-list-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="team-form" #form (submit)="onSubmit($event, form)"> <div class="gui-grid"> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="team" label="Team" required></gui-label> <div class="gui-widget"> <gui-list id="team" uid="team" label="Team" name="team" height="160" required [items]="teams" [value]="team()" (gui-visible-items-change)="onVisibleItemsChange($event)" (gui-change)="onChange($event)" > @for (item of visibleItems(); track item.value) { <div [id]="item.id" role="option" [attr.aria-selected]="item.selected" class="gui-list__item-wrapper" [style.height.px]="40" > <div class="gui-list__item" [class.gui-list__item-selected]="item.selected" [class.gui-list__item-focused]="item.focused" > {{ item.template }} </div> </div> } </gui-list> </div> </div> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Join team"></gui-button> </div> </div> <p id="team-result" role="status">{{ result() }}</p> </form> `,})export class ListNativeFormComponent { teams = ['Design', 'Engineering', 'Research', 'Support'];
team = signal<OptionValue | undefined>(undefined); visibleItems = signal<GuiVisibleItem[]>([]); result = signal('');
onVisibleItemsChange(event: Event) { this.visibleItems.set( (event as ListEvent<'gui-visible-items-change'>).detail, ); }
onChange(event: Event) { this.team.set((event as ListEvent<'gui-change'>).detail.value); }
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 type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/index.css';
const teams = ['Design', 'Engineering', 'Research', 'Support'];
const team = ref<OptionValue>();const visibleItems = ref<GuiVisibleItem[]>([]);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="team-form" @submit.prevent="onSubmit"> <div class="gui-grid"> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="team" label="Team" :required="true"></gui-label> <div class="gui-widget"> <gui-list id="team" uid="team" label="Team" name="team" :height="160" :required="true" :items.prop="teams" :value.prop="team" @gui-visible-items-change="visibleItems = $event.detail" @gui-change="team = $event.detail.value" > <div v-for="item in visibleItems" :key="item.value" :id="item.id" role="option" :aria-selected="item.selected" class="gui-list__item-wrapper" :style="{ height: '40px' }" > <div class="gui-list__item" :class="{ 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, }" > {{ item.template }} </div> </div> </gui-list> </div> </div> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Join team"></gui-button> </div> </div> <p id="team-result" role="status">{{ result }}</p> </form></template>import { LitElement, html } from 'lit';import { customElement, query, state } from 'lit/decorators.js';import { classMap } from 'lit/directives/class-map.js';import { repeat } from 'lit/directives/repeat.js';import { styleMap } from 'lit/directives/style-map.js';import type { GuiEventMap, GuiListEvents, GuiVisibleItem, OptionValue,} from '@golemui/gui-components';import '@golemui/gui-components/button';import '@golemui/gui-components/label';import '@golemui/gui-components/list';import '@golemui/gui-components/index.css';
type ListEvent<K extends keyof typeof GuiListEvents> = GuiEventMap< typeof GuiListEvents>[K];
const teams = ['Design', 'Engineering', 'Research', 'Support'];
@customElement('list-native-form')export class ListNativeForm extends LitElement { @query('#team-form') private form!: HTMLFormElement; @state() private team?: OptionValue; @state() private visibleItems: GuiVisibleItem[] = []; @state() private result = '';
protected override createRenderRoot() { return this; }
private onVisibleItemsChange(event: ListEvent<'gui-visible-items-change'>) { this.visibleItems = event.detail; }
private onChange(event: ListEvent<'gui-change'>) { this.team = event.detail.value; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); const data = Object.fromEntries(new FormData(this.form)); this.result = `Submitted ${JSON.stringify(data)}`; }
override render() { return html` <form id="team-form" @submit=${this.onSubmit}> <div class="gui-grid"> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="team" label="Team" required></gui-label> <div class="gui-widget"> <gui-list id="team" uid="team" label="Team" name="team" height="160" required .items=${teams} .value=${this.team} @gui-visible-items-change=${this.onVisibleItemsChange} @gui-change=${this.onChange} > ${repeat( this.visibleItems, (item) => item.value, (item) => html` <div id=${item.id} role="option" aria-selected=${item.selected} class="gui-list__item-wrapper" style=${styleMap({ height: '40px' })} > <div class=${classMap({ 'gui-list__item': true, 'gui-list__item-selected': item.selected, 'gui-list__item-focused': item.focused, })} > ${item.template} </div> </div> `, )} </gui-list> </div> </div> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Join team"></gui-button> </div> </div> <p id="team-result" role="status">${this.result}</p> </form> `; }}<form id="team-form"> <div class="gui-grid"> <div class="gui-grid__cell"> <div class="gui-field"> <gui-label uid="team" label="Team" required></gui-label> <div class="gui-widget"> <gui-list id="team" uid="team" label="Team" name="team" height="160" required></gui-list> </div> </div> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Join team"></gui-button> </div> </div> <p id="team-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#team-form'); const list = document.querySelector('#team');
list.addEventListener('gui-visible-items-change', (event) => { list.querySelectorAll('[role="option"]').forEach((option) => option.remove()); list.prepend(...event.detail.map(renderOption)); });
function renderOption(item) { const option = document.createElement('div'); option.id = item.id; option.setAttribute('role', 'option'); option.setAttribute('aria-selected', String(item.selected)); option.className = 'gui-list__item-wrapper'; option.style.height = '40px'; const text = document.createElement('div'); text.className = 'gui-list__item'; text.classList.toggle('gui-list__item-selected', item.selected); text.classList.toggle('gui-list__item-focused', item.focused); text.textContent = item.template; option.append(text); return option; }
list.items = ['Design', 'Engineering', 'Research', 'Support'];
form.addEventListener('submit', (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); document.querySelector('#team-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”Unlike the other elements, <gui-list> has a shadow root, which scrolls the items and holds them
in place. Your options are its children in the light DOM, so plain CSS styles them, and the
stylesheet styles the list itself:
<gui-list id="city" uid="city" role="listbox" tabindex="0" aria-label="City" aria-description="Where you live." aria-required="true" aria-invalid="true" aria-errormessage="city_errors" aria-activedescendant="city-item-2"> <!-- Shadow root: the scrolling box, which shows the items through its slots. --> <div id="city-item-0" role="option" class="gui-list__item-wrapper" aria-selected="false" > <div class="gui-list__item">Amsterdam</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">Berlin</div> </div> <div id="city-item-2" role="option" class="gui-list__item-wrapper" aria-selected="false" > <div class="gui-list__item gui-list__item-focused">Copenhagen</div> </div> <!-- One option per item in range. --> <div slot="errors" class="gui-list__errors"> <div class="gui-validator" id="city_errors" role="alert"> <div class="gui-validator__error">Pick the city you live in.</div> </div> </div></gui-list>The stylesheet styles these classes when your options use them:
| Class | Part |
|---|---|
gui-list__item-wrapper | An option. With aria-disabled="true" it is greyed out and ignores the pointer. |
gui-list__item | The text of an option, with its padding and hover color. |
gui-list__item-selected | On the text of the selected option. |
gui-list__item-focused | On the text of the active option. |
gui-list__item-disabled | On the text of a disabled option. |
gui-list__errors | The errors set on the list, under the items. |
gui-validator | The errors container. |
gui-validator__error | One error message. |
The list adds the errors container to its light DOM, in the errors slot, when you first set
errors, so that aria-errormessage can reach it. Keep it when you render your options: remove
and add only the options.
The shadow root has no parts to style and no tokens of its own: the box around the items is the
gui-list element itself, and height sets how tall it grows. Style the element and your
options from your stylesheet.
Tokens
Section titled “Tokens”The list reads these design tokens. Set them on :root to change every list, or on any element
to change the lists inside it:
| Token | What it styles |
|---|---|
--gui-bg-default | The list’s background. |
--gui-border-default | The list’s border. |
--gui-radius-md | The list’s corner radius. |
--gui-text-default | The items’ text. |
--gui-font-family | The font. |
--gui-font-sm | The text size of the items. |
--gui-intent-primary | The selected item’s background. |
--gui-intent-primary-hover | The background of the active item and the item under the pointer. |
--gui-intent-primary-text | The text of the selected, active and hovered items. |
--gui-bg-disabled | The background of disabled items. |
--gui-border-focus | The list’s border while focused. |
--gui-shadow-focus | The focus ring, around the list and the active item. |
--gui-intent-error | The list’s border and the messages when there are errors. |
--gui-shadow-focus-error | The focus ring when there are errors. |
--gui-font-xs | The text size of the errors. |
--gui-space-1 | The space around the errors. |
--gui-space-3 | The errors’ side padding. |
/* Calmer selection colors in every list. */gui-list { --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”- Listbox. The element itself is the listbox: it has
role="listbox"and takes the focus. The active item is announced througharia-activedescendant, so focus stays on the list while the arrow keys move. - Name.
labelnames the listbox througharia-label, andhintdescribes it througharia-description. Without a label the list has no accessible name. - Errors. While errors set on the list show, it has
aria-invalid="true"and points at them witharia-errormessage, and screen readers announce new errors as they appear. - States.
required,readonlyanddisabledsetaria-required,aria-readonlyandaria-disabled. A disabled list leaves the tab order. - Options. The ids, roles and
aria-selectedof your options are yours to set, as above.
| Key | Action |
|---|---|
| Tab | Moves focus to the list, on the selected item, and away from it. |
| ↓ ↑ | Moves to the next or previous item, skipping disabled ones. |
| Page Down Page Up | Moves by the number of items that fit in the list. |
| Home End | Moves to the first or last item that isn’t disabled. |
| Enter Space | Picks the active item, and fires gui-change. |
What every element shares, such as translating the browser messages, is in Accessibility.