Skip to content
Components, switch product

Item renderers

<gui-list> and <gui-multi-list> handle the keyboard, the focus, the selection and the scrolling, but they don’t draw their items. You render the items yourself, with your framework’s own markup, as children of the list. That way an item can hold anything: an icon, two lines of text, a price, a badge.

This list of 43 cities is rendered that way. The code is the same example in each framework:

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>
);
}

<gui-dropdown> and <gui-multi-dropdown> draw their options themselves. To change what an option shows, give them a renderItem function: see Dropdown options.

The list tells you which items to render. Whenever they change, it fires gui-visible-items-change with the items in event.detail, and visibleItems holds them too. Your code renders one option per item, as the list’s children, and does nothing else: the list keeps the active item and the selection, and picks an item on a click, on Enter and on Space. It reports only the items in view and a few on each side, so a list of 10,000 items still renders about 15 options.

Each item has everything its option needs:

KeyWhat it is
templateThe item as you gave it: a string, a number or your object, with all its keys to render.
valueThe item’s value, from value-field for objects.
indexThe item’s index in items.
idThe option’s id, <uid>-item-<index>.
selectedWhether the item is selected.
focusedWhether it is the active item, the one the arrow keys move.
disabledWhether it can’t be picked: it has disabled: true, or the whole list is disabled.

Keep your selected value in step from gui-change, as the examples do.

Each option is a child of the list, and:

  • its id is the item’s id: the list points aria-activedescendant at it to tell screen readers which item is active, and finds the item a click is on from it;
  • it has role="option", aria-selected from the item’s selected, and aria-disabled="true" when the item is disabled;
  • its height is item-height, 40px by default, since the list places the options from it.

Give the option the gui-list__item-wrapper class and wrap its content in gui-list__item, with gui-list__item-selected, gui-list__item-focused or gui-list__item-disabled as they apply, and the stylesheet styles it like the other fields. The classes are in the List page’s Styling section.

Key each option by item.value, so your framework keeps the options it already rendered as the user scrolls instead of rewriting them.

A <gui-multi-list> renders its options the same way. What changes is the value:

  • its value is values, an array, and an item is selected when its value is in it;
  • a click, Enter or Space adds the item’s value to values, or takes it out, and fires gui-item-toggle with the item’s value, then gui-input and gui-change with the new values.

An element with the gui-list__item-check class in the option’s text is a check box: the stylesheet shows its icon while the option has gui-list__item-selected.

Code of the example above
LanguagesField.tsx
import { useState } from 'react';
import { GuiLabel, GuiMultiList } from '@golemui/gui-components/react';
import type { GuiVisibleItem, OptionValue } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const languages = [
'Arabic', 'Bengali', 'Catalan', 'Chinese', 'Czech', 'Danish', 'Dutch',
'English', 'Finnish', 'French', 'German', 'Greek', 'Hebrew', 'Hindi',
'Hungarian', 'Indonesian', 'Italian', 'Japanese', 'Korean', 'Norwegian',
'Polish', 'Portuguese', 'Romanian', 'Russian', 'Spanish', 'Swahili',
'Swedish', 'Thai', 'Turkish', 'Ukrainian', 'Vietnamese',
];
export function LanguagesField() {
const [values, setValues] = useState<OptionValue[]>(['Catalan']);
const [visibleItems, setVisibleItems] = useState<GuiVisibleItem[]>([]);
return (
<>
<div className="gui-field">
<GuiLabel uid="languages" label="Languages you speak" />
<div className="gui-widget">
<GuiMultiList
id="languages"
uid="languages"
label="Languages you speak"
height={200}
items={languages}
values={values}
onGuiVisibleItemsChange={(event) =>
setVisibleItems(event.detail)
}
onGuiChange={(event) => setValues(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 adds the item or takes it out. */}
{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(' ')}>
<span className="gui-list__item-check" aria-hidden="true">
<svg viewBox="0 0 16 16" width="12" height="12">
<path d="M6.4 11.6 2.8 8l1.1-1.1 2.5 2.5 5.7-5.7 1.1 1.1z" />
</svg>
</span>
<span>{String(item.template)}</span>
</div>
</div>
);
})}
</GuiMultiList>
</div>
</div>
<p role="status">Selected: {values.join(', ') || 'none'}</p>
</>
);
}

A dropdown renders its options itself, from the same items: it builds each option’s id, role, aria-selected and height. renderItem only returns what the option shows. It gets the item, with your item in item.template, and the option’s state:

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

html is Lit’s template tag, passed in so that no framework needs to import Lit. It shows every value in ${} as text, never as markup. The function can also return a string, which shows as text, or a DOM node. This is the example from the Dropdown page:

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

The dropdown calls renderItem again whenever an option’s state changes: when the user moves to it, picks it or the search changes the list. Keep it fast and free of side effects.

A template covers most options. When an option needs a component of your framework, for its logic or because your design system already has one, render it into root, the element that holds the option’s content, and return nothing. The dropdown then leaves root to you. Call onCleanup with what destroys your component: the dropdown runs it when the option leaves the list, because the user scrolled, the search hid it or the list closed. The list only keeps the options in view, so without it every option the user scrolls past stays in memory.

The same option is reused while its item stays in view, so create the component once per root and update it on the next calls. The React and Angular examples keep their components in a WeakMap by root.

In this example, CurrencyOption is an ordinary component of each framework, with nothing in it for the dropdown: it takes a currency and whether it’s selected, and shows a check mark when it is. Each tab shows the component, then the field that renders it in the options:

Code of the example above
CurrencyOption.tsx
export type Currency = { code: string; name: string; symbol: string };
type Props = { currency: Currency; selected: boolean };
export function CurrencyOption({ currency, selected }: Props) {
return (
<span className="currency">
<span className="currency__symbol">{currency.symbol}</span>
{currency.name}
<span className="currency__code">
{selected && <span aria-hidden="true">✓ </span>}
{currency.code}
</span>
</span>
);
}
CurrencyField.tsx
import { createRoot, type Root } from 'react-dom/client';
import { GuiDropdown } from '@golemui/gui-components/react';
import type { GuiItemRenderer } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
import { CurrencyOption, type Currency } from './CurrencyOption';
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'];
// Each option is a React root, created on the first call and updated on the
// next ones. The dropdown runs onCleanup when the option leaves the list.
const roots = new WeakMap<HTMLElement, Root>();
const renderCurrency: GuiItemRenderer<Currency> = (
item,
{ root, selected, onCleanup },
) => {
let reactRoot = roots.get(root);
if (!reactRoot) {
const created = createRoot(root);
roots.set(root, created);
onCleanup(() => {
roots.delete(root);
created.unmount();
});
reactRoot = created;
}
reactRoot.render(
<CurrencyOption currency={item.template} selected={selected} />,
);
};
const currencyStyles = `
.currency {
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 CurrencyField() {
return (
<>
<GuiDropdown
id="currency-field"
label="Currency"
labelField="name"
valueField="code"
searchFields={searchFields}
value="EUR"
items={currencies}
renderItem={renderCurrency}
/>
<style>{currencyStyles}</style>
</>
);
}
  • Vue: bind items and value with the .prop modifier, so Vue sets the properties instead of attributes.
  • Lit: set the option’s height with styleMap, not a style attribute, so the example keeps working under a strict Content Security Policy.