SvListBox
An inline single or multi-select list - a WAI-ARIA listbox with a roving
highlight, full keyboard, type-ahead and optional windowing for huge option sets.
SvListBox is the always-visible list picker: no popover, no trigger, just the
options in place. It is controlled through value + onChange, groups options
by their group heading, and each row can be a custom itemTemplate. Turn on
virtual and it renders only the visible rows, so it scales to tens of thousands
of options while scroll and type-ahead stay instant. Colors come from the grid's
--sg-* tokens, so it matches the grid and the rest of the kit in light and dark.
Related: SvDropDownList · SvMultiSelect · Selection overview
Installation
Add it with the CLI - this drops a ready-to-edit SvListBox starter into your app:
Prefer to see it first? npx @svgrid/ui try list-box opens it in a throwaway sandbox - no project needed.
Or install the package and import it directly. SvListBox ships free in
@svgrid/grid (dependency-free):
import { SvListBox } from '@svgrid/grid'
Example
Open the live example: List box (Selection)
<script lang="ts">
import { SvListBox, type ListOption } from '@svgrid/grid'
const options: ListOption[] = [
{ value: 'eng', label: 'Engineering', group: 'Product' },
{ value: 'des', label: 'Design', group: 'Product' },
{ value: 'sal', label: 'Sales', group: 'Go-to-market' },
]
let value = $state<string | null>(null)
</script>
<SvListBox label="Team" {options} {value} onChange={(v) => (value = v)} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options |
ReadonlyArray<ListOption> |
- | The options. A group heading buckets them into sections. |
value |
string | number | Array<string | number> | null |
null |
Selected value(s). An array when multiple. |
onChange |
(value: any) => void |
- | Fires with the new value (scalar, or array when multiple). |
multiple |
boolean |
false |
Allow multiple selections (adds a checkmark column). |
rows |
number |
7 |
Visible height in rows before scrolling. |
virtual |
boolean |
false |
Window the list (render only visible rows) for large sets. Flat lists only. |
rowHeight |
number |
32 |
Fixed row height in px; must match the CSS row height. |
itemTemplate |
Snippet<[ListOption]> |
- | Custom per-option content. Receives the option. |
disabled |
boolean |
false |
Blocks interaction. |
label |
string |
- | Visible field label, wired to the control. |
hint |
string |
- | Helper text under the control. |
error |
string |
- | Error message; announced and styled when set. |
required |
boolean |
false |
Marks the field required. |
invalid |
boolean |
false |
Applies the invalid state. |
name |
string |
- | Emits hidden input(s) carrying the value for form posts. |
dir |
ltr | rtl | auto |
auto |
Text direction. |
ariaLabel |
string |
- | Accessible name when there is no visible label. |
id |
string |
- | Root id; label/hint/error ids derive from it. |
ListOption
type ListOption = {
value: string | number
label: string
disabled?: boolean
group?: string // optional section heading
color?: string // optional CSS color swatch shown before the label
}
Examples
Multi-select with a summary
Set multiple and read back the array in onChange to drive a live count or
chip summary:
<script lang="ts">
let picked = $state<Array<string | number>>([])
</script>
<SvListBox label="Assignees" {options} multiple
value={picked} onChange={(v) => (picked = v)} />
<p>{picked.length} selected</p>
Custom rows
Pass an itemTemplate snippet to render richer options (avatar, role, meta) while
keeping the list's keyboard and selection behavior:
<SvListBox {options}>
{#snippet itemTemplate(opt)}
<Avatar name={opt.label} /> <span>{opt.label}</span>
{/snippet}
</SvListBox>
Virtualized huge lists
For thousands of options, add virtual (and keep rowHeight in sync with the
CSS). Only the visible rows hit the DOM, so scrolling never reflows:
<SvListBox {options} virtual rowHeight={32} />
Note: on a very fast scrollbar-thumb drag the viewport can outrun windowing by many rows in a single frame. Rather than flash blank, the off-screen area shows faint placeholder rows (a skeleton) that the real rows paint over a frame later, so the list stays visually filled at any scroll speed.
Tip:
virtualonly takes effect on a flat list - giving options agroupturns windowing off (grouped rows are variable-height), so keep large virtualized lists ungrouped.
Cascading lists
Derive the second list from the first pick, and clear the child when the parent changes so a stale value never lingers:
<script lang="ts">
import { SvListBox, type ListOption } from '@svgrid/grid'
const teams: ListOption[] = [
{ value: 'eng', label: 'Engineering' },
{ value: 'des', label: 'Design' },
]
const membersByTeam: Record<string, ListOption[]> = {
eng: [{ value: 'ada', label: 'Ada' }, { value: 'linus', label: 'Linus' }],
des: [{ value: 'jony', label: 'Jony' }],
}
let team = $state<string | number | null>(null)
let member = $state<string | number | null>(null)
const members = $derived(team ? membersByTeam[String(team)] ?? [] : [])
</script>
<SvListBox label="Team" options={teams} value={team}
onChange={(v) => { team = v; member = null }} />
<SvListBox label="Member" options={members} value={member}
onChange={(v) => (member = v)} />
Accessibility
- Renders a real
role="listbox"with a roving highlight;ArrowUp/ArrowDownmove,Home/Endjump, and typing does type-ahead. - Selection state is exposed via
aria-selected;multipleaddsaria-multiselectable. label,hint, anderrorare wired througharia-describedby; passariaLabelwhen there is no visible label.
More examples
Virtualized list
SvListBox scaling to 50,000 options via fixed-row windowing (only visible rows in the DOM) - scroll + type-ahead stay instant. Plus a custom itemTemplate row (avatar + role). Just add virtual.
Open the live example: Virtualized list (Selection)
Sizes
Every control takes the same three sizes, so a dense toolbar and a roomy form can share components.
<script lang="ts">
import { SvListBox } from '@svgrid/grid'
let picked = $state('')
</script>
<SvListBox value={picked} size="sm" />
<SvListBox value={picked} size="md" />
<SvListBox value={picked} size="lg" />
In a form
The shared field props behave the same on every editor: label names it, hint explains it, and error plus invalid mark it - which is why a validated form does not need per-component handling.
<script lang="ts">
import { SvListBox } from '@svgrid/grid'
let picked = $state('')
</script>
<SvListBox
value={picked}
label="Label"
hint="A short hint"
required
/>
<SvListBox
value={picked}
label="Label"
error="Something is wrong"
invalid
/>
See also
- Selection overview - the whole picker family at a glance.
- SvDropDownList - the same single-select model in a popover.
- SvMultiSelect - a multi-select collapsed into a chip trigger.
Live examples
- List box - SvListBox: an assignee picker - inline multi-select with grouped options (departments), roving keyboard + type-ahead and a live summary.
- Virtualized list - SvListBox scaling to 50,000 options via fixed-row windowing (only visible rows in the DOM) - scroll + type-ahead stay instant. Plus a custom itemTemplate row (avatar + role). Just add virtual.