1.1 Strings
Simple string-typed options.
code
Run npm run build, then npm run serve, and open /demo/.
Simple string-typed options.
Generic T = User object. itemToStringFn setting maps it to its display string (no subclass).
clearable: true adds an x in its own trigger slot. Click
it: value clears to undefined, onChange
fires. Custom icon: createTriggerClearButtonContentElFn.
Listbox uses position: fixed; tracks the trigger on scroll/resize.
Flips up when there is not enough room below.
Open the dropdown, then scroll the gray box in either direction. Scrolling clips the trigger above or below the container edge - the popup should auto-close in both cases.
The amber box's transform would normally displace a
position: fixed popup. With the Popover API (all current
browsers) the popup renders in the top layer instead - open it: it
must align under the trigger. Old browsers show the documented
displacement.
Open the dropdown, then click the outside button. The two modes differ in whether the outside click also triggers its own action.
Outside click closes the dropdown AND triggers the button. Count below should increment.
Outside click closes the dropdown only; the button does NOT receive the click. Count below should stay 0.
Library ships no arrow by default. Pass createTriggerArrowContentElFn
to add any element (SVG, <i> icon class, plain text, ...).
All variants below use the same data binding; only the arrow differs.
Both return SVGElement with fill="currentColor" and
aria-hidden="true". CSS rotates them on
[data-state="open"].
createChevronDownSvgEl()createTriangleDownSvgEl()Just return an <i class="mdi mdi-...">. No lib API needed.
Pure CSS pseudo-element on .llselect-trigger. data-state drives rotation.
Arrow and clear x live in separate trigger slots, so they compose -
and both survive a custom createTriggerContentElFn.
Click items to toggle; the popup stays open. Items carry
aria-selected, the list aria-multiselectable.
Default trigger content - "n / m selected" / "All n selected" / placeholder.
External buttons call lib methods.
triggerDisplay: 'tags' - one removable chip per chosen
item; the library owns the chip, its x button and
aria-label. Custom chip content / x icon: see 11.4.
createItemContentElFn renders checkbox + item text; the
public isChosen() supplies the state (the fn runs per
render, so the self-reference in the code is safe); the single-item
rerender keeps it in sync. Subclass equivalent: 13.1.
chooseAllRow: true pins a tri-state row acting on the
VISIBLE enabled subset: type a filter, toggle the row - only the
matches change. createChooseAllRowContentElFn swaps the
default plain-text row (5.7) for the outlined SVG; items reuse
5.4's checkbox.
5.5 drawn with createFilledCheckboxSvgEl (tick / dash cut
out of a solid box); same settings checkbox as 5.4. Choose a few
items - the choose-all row shows the filled indeterminate state.
No content fn: the row is just the counting text, and its numbers ARE the tri-state. The library draws no default indicator anywhere; 5.5 / 5.6 add SVG checkboxes via the hook.
A chosen item's row leaves the popup; removing its tag puts it back, and the clear button puts everything back at once. Natural with tags: the trigger shows what is chosen, the popup what is still choosable. Once everything is chosen the popup shows the no-results text.
Trigger width is your CSS's job; chosen text in it ellipsizes. The popup
defaults to popupWidthPolicy: 'fit-content' (grows to its
content, never narrower than the trigger, viewport-clamped - native
select behavior); 'match-trigger' pins it to the trigger
width.
Demo CSS: none. Pick the long sentence - the trigger grows to fit and overflows the pane: the library constrains nothing; tidy layout is the caller's CSS job.
'fit-content': popup outgrows the trigger
Demo CSS constrains the mount to max-width: 18rem: the
trigger ellipsizes, but the default popup still opens at the widest
item text's width (viewport-clamped).
'match-trigger': item text wraps
Same 18rem constraint, opt-in
'match-trigger': popup pinned to the trigger width, long
item text wraps.
Demo CSS adds text-overflow: ellipsis on the items
(needs 'match-trigger' - a fit-content popup never
truncates); a createItemEl subclass sets
el.title. The subclass IS required for this exact
effect: no setting reaches the option element itself, so a content fn
could only tooltip the inner text, not the whole row. (The library never
auto-adds title - bring your own tooltip lib, or this.)
filterable: true adds a filter input inside the popup. Esc
clears the filter, then closes; IME composition is respected.
Default filterFn is case-insensitive substring on the
item's itemToStringFn string.
Filtering does not change the chosen set: a chosen item hidden by the current query stays chosen and reappears when cleared. Checkboxes: 5.4's settings pattern.
filterFn (match name OR role)
A custom filterFn matches name OR role - the default
substring only sees the rendered text.
createHighlightedTextEl)
The exported helper wraps each query match in <mark>.
createItemContentElFn re-runs on every keystroke, so the
marks follow the query; the option's aria-label stays the
plain text.
10,000 rows each. Lazy render builds the option DOM on open and clears it on close.
Open and close must stay instant.
Toggling re-renders only that item's element (O(1) DOM), not the 10k list - click items rapidly.
Always aria-disabled, never the native disabled
attribute - disabled things stay hoverable, so a tooltip can explain
WHY.
itemDisabledFn + why-tooltip (createItemEl subclass)
Out-of-stock drinks: unselectable, skipped by arrows, still hoverable
- hover one for the tooltip a createItemEl subclass
attached. The subclass IS required here: the title sits
on the option element itself (whole-row hover), and
isItemEffectivelyDisabled() is protected - neither is
reachable from settings.
setDisabled + focusableWhenDisabled
First control: setDisabled() removes it from the tab
order (default). Second: focusableWhenDisabled: true
keeps it Tab-reachable, so keyboard users get the tooltip too.
focusableWhenDisabled: true (press Tab to reach it)A disabled control must not change value through ANY of its buttons. Toggle the disable and try the tag x buttons and the clear x: while disabled they do nothing; re-enabled they work again.
itemToGroupKeyFn maps each item to a group key (contiguous
same-key items form one group - pre-sort your data);
groupKeyToStringFn is the display / i18n customization point. Keyboard nav
skips labels. Custom label content: 11.3.
Foods grouped by category. Open it - each category shows a label above its items.
groupDisabledFn disables the whole Dairy group (still
hoverable). Type to filter - survivors regroup, empty groups
disappear.
The no-subclass half of the customization model: each
create*ContentElFn setting fills an element's VISIBLE
content only; the element itself and its ARIA stay library-built (an
option's aria-label stays the plain
itemToString). When the library-built element itself must
change, subclass instead: section 13.
Icon + text rows via createItemContentElFn. The trigger
mirrors the chosen row only because the same renderer feeds
createTriggerContentElFn - there is no auto-projection.
Same renderer on a multiple; the option element keeps
aria-selected plus the auto aria-label.
createGroupLabelContentElFn fills the label (icon +
item count); the accessible name stays the plain
groupKeyToString.
11.1's renderer feeds createTagContentElFn too - ONE
function serves rows AND chips.
createTagRemoveButtonContentElFn swaps the x icon; unset,
the theme's CSS glyph draws it.
Primary text + a faded .hint pushed to the row's right
edge (.user-row); the name highlights the filter match
(createHighlightedTextEl). Visual only: the option's
aria-label stays the plain itemToString name.
What <option style="background-color"> does on a
native select (Chromium only): every row tinted from its icon color,
and the chosen item's colors cover the whole trigger
(onChange paints the public triggerEl).
The tint is translucent, so hover / keyboard-focus stay visible.
The checkmark is createCheckmarkSvgEl; rows re-render
on chosen changes, so it never goes stale. All inline styles, plus
one demo CSS line zeroing the option element's padding.
Native <label for> cannot target a div, so the
labelEl setting emulates both halves: it feeds the
accessible name (via aria-labelledby) and clicking it
focuses the trigger - focus only, like a native select label.
aria-labelledby referencing the label
Settings customize CONTENT; subclassing customizes the library-built
elements (e.g. the role="option" element itself) and can
read protected state. Performance is identical (one shared prototype method vs one
closure per instance is noise) - pick by capability. The
required-subclass cases live with their features: 6.4 (title on the
whole row), 9.1 (protected isItemEffectivelyDisabled()).
The same result as 5.4 via a createItemEl override - not
required, kept as the reference example of the subclass surface.
Branch rows expand / collapse via the caret, wear a folder icon that
follows the state, and show a tri-state checkbox derived from their
leaves; activating a branch toggles its whole subtree. The model value is the chosen leaves. Built on the
extension points: typed subclass settings
(defaultExpandDepth), a cached
getVisibleItems override, createItemEl /
createItemContentEl, onItemActivated. The
toggle shows the usage, the TypeScript subclass
(subclass/tree-select.ts), and its demo CSS.
Content that stays put while the list scrolls. Built once, in the constructor; the returned node is yours to update. No ARIA role, and never part of the listbox.
The trigger shows chips, so the header takes over the count with triggerCountSummary. The match count is rewritten from onFilterQueryChange. The Clear all button is a slot control. A click keeps keyboard input on the combobox. Tab reaches it while the popup is open.
Rows show a name and a role; the header repeats the same layout in bold. It sits between the filter input and the list, so the headings stay while the rows scroll.
A pinned link under the list. A click keeps keyboard input on the combobox; Tab reaches the link while the popup is open, and Esc on it returns to the trigger.
The same two commands as 15.1 with the same row look, only pinned: they never scroll away. Each is a real button wearing the theme's item class. Tab reaches them, but they sit outside the arrow-key ring and are not announced as options; the rows in 15.1 are the keyboard-native form.
Rows the library renders as options above or below the items, so a keyboard user reaches them with the arrow keys. They scroll with the list. Each row is a plain object: its text, whether it is disabled, and what activation does. The library never chooses or closes for it.
The row reads the live selection: textFn and disabledFn run again after every change. It follows the choose-all row, so ArrowDown reaches it before any item.
A single select with one row above the items. Activating it asks for a name, adds it, picks it and closes; the row itself never closes the popup.
The row flips the order with setItems and shows the current one. A filter query re-applies to the new order, and the chosen item stays chosen.
The rows of 15.1 with popupListLeadingRowsPinned: true: the choose-all row and the command stay in view while the items scroll under them, and keep their place in the arrow-key ring.
All chrome strings live in the uiTranslationPack setting;
packs from @llselect/core/i18n fill it whole, per-key
overrides via spread. An explicit placeholder setting wins
over the pack's default.
Switch the pack - setUiTranslationPack re-renders in
place, chosen values survive. Placeholders, filter input, clear x,
tag remove buttons and the count summary all translate.
ar / he also set dir="rtl" on
the mounts (direction is inherited - there is no RTL setting): arrows
and x buttons mirror LEFT, chips flow right-to-left.
LLSelectSingle - localized placeholder, chosen text in the triggertriggerDisplay: 'tags' - translated remove x per chip'count' - translated count summary
dir="rtl" on the mount is ALL the wiring - there is no
RTL API. Flex rows and logical margins mirror by themselves: the
checkboxes land on the right, the ar pack's counting text reads
right-to-left.
One instance per entry in uiTranslationPackByLocale - the
grid grows with the library. Each trigger shows its pack's own
placeholder; RTL packs set dir="rtl".