You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/components/listbox
dev c499dd239a
Consolidate Soma interaction runtimes
5 months ago
..
components remove backward-compat re-export shims 5 months ago
README.md soma: 10 new components + reactivity audit + guide rules A30–A33 6 months ago
exports.ts soma: 10 new components + reactivity audit + guide rules A30–A33 6 months ago
index.ts soma: 10 new components + reactivity audit + guide rules A30–A33 6 months ago
langs.ts soma: 10 new components + reactivity audit + guide rules A30–A33 6 months ago
listbox-provider.svelte.ts Consolidate Soma interaction runtimes 5 months ago
types.ts Route UIX managed DOM writes through ActiveDom 5 months ago

README.md

Listbox

A standalone role="listbox" with roving-tabindex keyboard navigation, alphanumeric typeahead, single or multiple selection, ARIA groups, and native form integration. Use it when the list is embedded in the layout (sidebars, file pickers, command palettes where the list is always open). For dropdown lists inside a popover, use Select instead — Select already embeds listbox semantics.

Anatomy

<Listbox.Provider bind:value aria-label="Fruits">
	<Listbox.Item value="apple">
		Apple
		<Listbox.ItemIndicator>✓</Listbox.ItemIndicator>
	</Listbox.Item>
	<Listbox.Item value="banana">Banana</Listbox.Item>

	<Listbox.Group>
		<Listbox.GroupLabel>Citrus</Listbox.GroupLabel>
		<Listbox.Item value="lemon">Lemon</Listbox.Item>
		<Listbox.Item value="orange">Orange</Listbox.Item>
	</Listbox.Group>
</Listbox.Provider>

Parts

Part Element Description
Provider <div> Root with role="listbox". Holds state + keyboard + registry.
Item <div> One option. role="option", aria-selected, roving tabindex.
ItemIndicator <span> Renders only when the parent Item is selected (unless forceMount).
Group <div> ARIA group container. Pair with GroupLabel.
GroupLabel <div> Heading that labels the surrounding Group.

Props

Provider

Prop Type Default Description
id string auto DOM id.
value string[] [] Bindable selection (always an array; in single mode holds ≤1 entry).
onValueChange (v: string[]) => void — Fires on every selection change.
selectionMode 'single' | 'multiple' 'single' How Enter/Space and click behave.
loop boolean true Arrow-key wrapping at ends.
typeahead boolean true Alphanumeric typeahead jump.
typeaheadTimeout number 500 ms before the typeahead buffer resets.
orientation 'horizontal' | 'vertical' 'vertical' Layout + which arrow keys step.
dir 'ltr' | 'rtl' from Soma Reading direction. Flips horizontal arrows.
disabled boolean false OR-merged with Field.Provider.
readonly boolean false OR-merged.
required boolean false OR-merged.
invalid boolean false OR-merged.
name string — When set, renders one hidden input per selected value for form submission.
aria-label string translated Accessible label. Overrides the translated default (Options).
aria-labelledby string — Id of an external label. When set, aria-label is omitted.

Item

Prop Type Default Description
id string auto DOM id.
value string — Required. Selection key. Distinct values required within a single listbox.
disabled boolean false Item-specific disable (ORed with Provider).
textValue string — Text used for typeahead when it differs from textContent.

ItemIndicator

Prop Type Default Description
id string auto DOM id.
forceMount boolean false Keep in DOM when unselected — useful for CSS exit transitions.

Snippet props

// Provider
{ value: string[]; isEmpty: boolean; isFocused: boolean }

// Item
{ isSelected: boolean; isHighlighted: boolean; isDisabled: boolean; value: string }

ARIA

Part Attribute Value
Provider role listbox
Provider aria-orientation horizontal | vertical
Provider aria-multiselectable true when selectionMode='multiple'
Provider aria-label Translated default (Options) unless overridden
Provider aria-labelledby When aria-labelledby prop is set
Provider aria-disabled / -readonly / -required / -invalid when respective flag is on
Item role option
Item aria-selected true | false
Item aria-disabled when disabled
Item tabindex 0 on the roving target, -1 on the rest
Group role group
Group aria-labelledby DOM id of the sibling GroupLabel

Exactly one Item holds tabindex=0 at any time: the first selected + enabled item, or (if nothing is selected) the first enabled item.

Data Attributes

Part Attribute Values
Provider data-listbox Always present
Provider data-orientation horizontal | vertical
Provider data-disabled when disabled
Provider data-readonly when readonly
Provider data-required when required
Provider data-invalid when invalid
Provider data-focused while focus is inside
Provider data-empty when value.length === 0
Item data-listbox-item Always present
Item data-state selected | unselected
Item data-highlighted while pointer/focus is on it
Item data-disabled when disabled
Item data-orientation inherited from Provider
Item data-value the item's value
ItemIndicator data-listbox-item-indicator Always present
ItemIndicator data-state selected | unselected
Group data-listbox-group Always present
GroupLabel data-listbox-group-label Always present

Keyboard

Key Action
ArrowDown / ArrowUp Move focus to next / previous enabled item (vertical orientation).
ArrowRight / ArrowLeft Same, horizontal orientation. RTL flips these.
Home / End Jump to first / last enabled item.
Enter / Space Toggle selection of the focused item.
A…Z / 0…9 Typeahead: jump to the next item whose text starts with the typed prefix. Repeat to cycle.
Ctrl/Cmd+A Select all enabled items (only when selectionMode='multiple').
Click Select (single) / toggle (multiple) — focus moves to the clicked item.

Disabled items are skipped entirely by arrow nav and typeahead.

i18n

Key English Spanish
label Options Opciones

Override per-instance with the aria-label or aria-labelledby prop.

Comparison

Feature Soma Radix Ark UI bits-ui React Aria
Dedicated component (not inside Select) ✅ ❌¹ ✅ ❌² ✅
Single + multiple ✅ — ✅ — ✅
Roving tabindex ✅ — ✅ — ✅
Alphanumeric typeahead ✅ — ✅ — ✅
Ctrl/Cmd+A select-all (multi) ✅ — ❌ — ✅
Groups (role="group" + label) ✅ — ✅ — ✅
Horizontal orientation ✅ — ✅ — ✅
RTL-aware arrow keys ✅ — ✅ — ✅
ItemIndicator (for check marks) ✅ — ✅ — ✅
Hidden inputs for native form ✅ — ❌ — ❌
Field OR-merge ✅ — ❌ — ❌
Translated default aria-label ✅ — ⚠️ — ⚠️

¹ Radix embeds listbox semantics in Select.Content (not reusable standalone). ² bits-ui's Listbox is built on top of their Select internals; no separate export.

Usage

Single-select

<script lang="ts">
	import { Listbox } from '$soma/components';
	let value = $state<string[]>(['apple']);
</script>

<Listbox.Provider bind:value aria-label="Fruit">
	<Listbox.Item value="apple">Apple</Listbox.Item>
	<Listbox.Item value="banana">Banana</Listbox.Item>
	<Listbox.Item value="cherry">Cherry</Listbox.Item>
</Listbox.Provider>

Multi-select with indicators

<Listbox.Provider bind:value selectionMode="multiple" aria-label="Toppings">
	{#each toppings as t (t.value)}
		<Listbox.Item value={t.value}>
			<span>{t.label}</span>
			<Listbox.ItemIndicator>✓</Listbox.ItemIndicator>
		</Listbox.Item>
	{/each}
</Listbox.Provider>

Grouped

<Listbox.Provider bind:value aria-label="Team members">
	<Listbox.Group>
		<Listbox.GroupLabel>Engineering</Listbox.GroupLabel>
		<Listbox.Item value="a">Alice</Listbox.Item>
		<Listbox.Item value="b">Bob</Listbox.Item>
	</Listbox.Group>
	<Listbox.Group>
		<Listbox.GroupLabel>Design</Listbox.GroupLabel>
		<Listbox.Item value="c">Carla</Listbox.Item>
	</Listbox.Group>
</Listbox.Provider>

Inside a form

<form onsubmit={onSubmit}>
	<Listbox.Provider bind:value selectionMode="multiple" name="roles">
		<Listbox.Item value="admin">Admin</Listbox.Item>
		<Listbox.Item value="editor">Editor</Listbox.Item>
		<Listbox.Item value="viewer">Viewer</Listbox.Item>
	</Listbox.Provider>
	<button type="submit">Save</button>
</form>

name="roles" produces one <input type="hidden" name="roles" value="…"> per selected entry; the browser appends duplicates to FormData.

Out of scope (v2 roadmap)

Explicit list of features the user signed off as not in the v1 release — documented so a future PR can pick them up without re-litigating scope.

Shift+Arrow range-extension (multi-select)

What: In selectionMode='multiple', hold Shift while pressing ArrowDown/ArrowUp to extend the selection from the previously selected item to the focused item — and Shift+Home / Shift+End to extend to the boundary.

Reference libraries: React Aria implements it; Radix doesn't expose Listbox standalone; Ark has a partial implementation (range via click but not keyboard); bits-ui omits it.

Why out-of-scope in v1: Range extension requires tracking an "anchor" item (the starting point of the range), swapping it when the user clicks with a modifier, and layering the range logic on top of the existing keyboard nav. Not hard in isolation but it doubles the complexity of the keydown handler. Ctrl/Cmd+A (select-all) covers the primary "bulk select" use case for v1.

Cost estimate: ~60 lines in the provider (anchor state + 4 branch keys in handleItemKeydown) + keyboard table row + README example. No breaking change.

Implementation notes

  • Items register themselves on mount via the Provider's registry rather than querySelectorAll. Mounted groups, lazy-rendered rows and portaled content all work without extra bookkeeping.
  • DOM order is enforced with Node.compareDocumentPosition on the registry — insertion order can differ when items are conditionally rendered.
  • Typeahead buffer: repeated same-letter keypresses cycle through matches; a multi-letter prefix ("ba", "ban") matches from the current index, not the next. Buffer resets after typeaheadTimeout ms with no further key input.
  • Roving-tabindex target is picked on every render: prefer the first selected+enabled item, fall back to the first enabled item. This is stable as items appear/disappear.
  • In single mode, clicking the already-selected item keeps it selected (no unintentional clear). Use value = [] manually (or an explicit Clear button) to reset.
  • Multi-selection with Shift+Arrow range-extension is not implemented — reserved for a future version. Ctrl/Cmd+A covers the "select all" use case, and clicks / Enter / Space toggle per item.

Powered by TurnKey Linux.