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: 14.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 headers. Custom header content: 11.3.
Foods grouped by category. Open it - each category shows a header 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 14.
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 header (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
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".
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.