Skip to content
Components, switch product

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.

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

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.

CityField.tsx
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 ?? '')}
/>
);
}

Then render the items in range as its children, as below.

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.

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.

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

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:

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

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.

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.

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

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

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

ClassPart
gui-list__item-wrapperAn option. With aria-disabled="true" it is greyed out and ignores the pointer.
gui-list__itemThe text of an option, with its padding and hover color.
gui-list__item-selectedOn the text of the selected option.
gui-list__item-focusedOn the text of the active option.
gui-list__item-disabledOn the text of a disabled option.
gui-list__errorsThe errors set on the list, under the items.
gui-validatorThe errors container.
gui-validator__errorOne 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.

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

TokenWhat it styles
--gui-bg-defaultThe list’s background.
--gui-border-defaultThe list’s border.
--gui-radius-mdThe list’s corner radius.
--gui-text-defaultThe items’ text.
--gui-font-familyThe font.
--gui-font-smThe text size of the items.
--gui-intent-primaryThe selected item’s background.
--gui-intent-primary-hoverThe background of the active item and the item under the pointer.
--gui-intent-primary-textThe text of the selected, active and hovered items.
--gui-bg-disabledThe background of disabled items.
--gui-border-focusThe list’s border while focused.
--gui-shadow-focusThe focus ring, around the list and the active item.
--gui-intent-errorThe list’s border and the messages when there are errors.
--gui-shadow-focus-errorThe focus ring when there are errors.
--gui-font-xsThe text size of the errors.
--gui-space-1The space around the errors.
--gui-space-3The errors’ side padding.
styles.css
/* 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.

  • Listbox. The element itself is the listbox: it has role="listbox" and takes the focus. The active item is announced through aria-activedescendant, so focus stays on the list while the arrow keys move.
  • Name. label names the listbox through aria-label, and hint describes it through aria-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 with aria-errormessage, and screen readers announce new errors as they appear.
  • States. required, readonly and disabled set aria-required, aria-readonly and aria-disabled. A disabled list leaves the tab order.
  • Options. The ids, roles and aria-selected of your options are yours to set, as above.
KeyAction
TabMoves 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 UpMoves by the number of items that fit in the list.
Home EndMoves to the first or last item that isn’t disabled.
Enter SpacePicks the active item, and fires gui-change.

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