<!-- https://svgrid.com/docs/help/ui-components/sv-list-box/ - SvGrid documentation as markdown. Index of every page: https://svgrid.com/llms.txt -->

# 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](https://svgrid.com/docs/help/ui-components/sv-drop-down-list/) · [SvMultiSelect](https://svgrid.com/docs/help/ui-components/sv-multi-select/) · [Selection overview](https://svgrid.com/docs/help/ui-components/selection/)

## Installation

Add it with the CLI - this drops a ready-to-edit `SvListBox` starter into your app:

<div data-docs-add="add list-box"></div>

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):

<div data-docs-install="@svgrid/grid"></div>

```ts
import { SvListBox } from '@svgrid/grid'
```

## Example

> Live demo: List box - https://svgrid.com/demos/312-listbox/

```svelte
<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

```ts
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:

```svelte
<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:

```svelte
<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:

```svelte
<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: `virtual` only takes effect on a flat list - giving options a `group`
> turns 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:

```svelte
<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` / `ArrowDown`
  move, `Home` / `End` jump, and typing does type-ahead.
- Selection state is exposed via `aria-selected`; `multiple` adds
  `aria-multiselectable`.
- `label`, `hint`, and `error` are wired through `aria-describedby`; pass
  `ariaLabel` when 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.

> Live demo: Virtualized list - https://svgrid.com/demos/323-list-virtualization/

## Sizes

Every control takes the same three sizes, so a dense toolbar and a roomy form can share components.

```svelte
<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.

```svelte
<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](https://svgrid.com/docs/help/ui-components/selection/) - the whole picker family at a glance.
- [SvDropDownList](https://svgrid.com/docs/help/ui-components/sv-drop-down-list/) - the same single-select model in a popover.
- [SvMultiSelect](https://svgrid.com/docs/help/ui-components/sv-multi-select/) - a multi-select collapsed into a chip trigger.

---

SvGrid is the Svelte 5 data grid (`npm install @svgrid/grid`, MIT). This page: https://svgrid.com/docs/help/ui-components/sv-list-box/ . All docs: https://svgrid.com/llms.txt
