Listbox Grid
Accessible, unstyled 2-D option grid (icon, color, and swatch pickers) with typed selection. The pure grid navigation (moveInGrid, gridMoveFromKey) stays internal to the family source so the subpath bundle keeps a single chunk order; source installs can import listbox-grid-model.ts directly.
| Package | @vizejs/ui/listbox-grid |
| Maturity | stable |
| Own the source | vize lib pull listbox-grid |
| Requires | collection, context, controllable-state, id, typeahead |
| Aliases | listbox grid, icon picker, color swatch picker, rich select grid |
| Covers | WAI-ARIA listbox pattern (grid layout), React Aria ListBox grid layout, Ark UI Color Picker swatches |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { ListboxGrid, ListboxGridItem } from "@vizejs/ui/listbox-grid";
Or copy the source into your project with vize lib pull listbox-grid (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Single-select color swatch grid navigated with arrow keys in two dimensions. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import { ListboxGrid, ListboxGridItem } from "@vizejs/ui/listbox-grid";
const swatches = ["Crimson", "Coral", "Gold", "Olive", "Teal", "Navy", "Plum", "Slate"];
const color = ref<string | null>("Teal");
const labelId = useId();
</script>
<template>
<div>
<span :id="labelId">Label color</span>
<ListboxGrid v-model="color" :columns="4" name="labelColor" :aria-labelledby="labelId">
<ListboxGridItem v-for="swatch in swatches" :key="swatch" :value="swatch">
{{ swatch }}
</ListboxGridItem>
</ListboxGrid>
<output>Selected: {{ color ?? "none" }}</output>
</div>
</template>
API
ListboxGrid<T, Multiple extends boolean = false>
Source: src/families/selection/listbox-grid/listbox-grid.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | null |
undefined |
Consumer-owned listbox id. null and undefined select a deterministic fallback. |
modelValue |
ListboxGridModelValue<T, Multiple> |
undefined |
Controlled selection: T | null, or readonly T[] when multiple is true. |
defaultValue |
ListboxGridModelValue<T, Multiple> |
undefined |
Initial selection for uncontrolled use and the value restored by reset. |
multiple |
Multiple |
false |
Allow selecting several options. The literal type decides the model type. |
items |
readonly T[] |
undefined |
Option values in display order; used for inference and select-all. |
by |
ListboxGridBy<T> |
undefined |
Compare values by a property key or with a custom equality function. |
columns |
number |
4 |
Options per row; must match the consumer's CSS grid so arrow keys follow the layout. |
pageRows |
number |
3 |
Rows traversed by PageUp and PageDown. |
selectionFollowsFocus |
boolean |
false |
Select the option that becomes active through arrow keys (single mode, swatch pickers). |
disabled |
boolean |
false |
Disable every option and remove the grid from the tab order. |
required |
boolean |
false |
Mark a selection as required for assistive technology. |
name |
string |
undefined |
Form field name; each selected value submits one hidden input. |
form |
string |
undefined |
Id of the owning form when rendered outside it. |
formValue |
(value: T) => string |
undefined |
Serialize a value for form submission. |
dir |
"ltr" | "rtl" |
"ltr" |
Reading direction; rtl mirrors ArrowLeft and ArrowRight. |
typeaheadTimeout |
number |
500 |
Idle time before buffered typeahead starts a new query. |
ariaLabel |
string |
undefined |
Accessible name when no visible label supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the grid. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the grid. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: ListboxGridModelValue<T, Multiple>] |
Fired when the selection requests a new controlled value (T | null or readonly T[]). |
change |
[ value: ListboxGridModelValue<T, Multiple>, previous: ListboxGridModelValue<T, Multiple>, nativeEvent: Event, ] |
Fired after a user interaction changes the selection: next value, previous value, native event. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ListboxGridSlotState<T> |
ListboxGridItem options. Receives selection, active option, and layout state. |
Exposed
| Member | Type | Description |
|---|---|---|
selected |
readonly T[] |
Selected values. |
activeId |
string | null |
Id of the highlighted option. |
focus |
(options?: FocusOptions) => void |
Move DOM focus to the grid. |
select |
(value: T) => boolean |
Select (single) or toggle (multiple) a value. |
clear |
() => boolean |
Clear the selection. |
reset |
() => boolean |
Restore defaultValue. |
ListboxGridItem<T>
Source: src/families/selection/listbox-grid/listbox-grid-item.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | null |
undefined |
Consumer-owned option id. null and undefined select a deterministic fallback. |
value (required) |
T |
required |
Option value compared with the selection through by. |
disabled |
boolean |
false |
Disable this option. |
textValue |
string |
undefined |
Explicit typeahead text; icon and color swatches usually need one. |
ariaLabel |
string |
undefined |
Accessible name, required when the option renders only an icon or swatch. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
ListboxGridItemSlotState<T> |
Option content. Receives position, selection, and highlight state. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
selected |
— | |
active |
— |
Behavior
Contract
ListboxGrid is a headless, strongly typed 2-D option grid for icon, color,
swatch, and "rich select" pickers. listbox-grid.vue is generic over the value
type T and a Multiple literal (T | null or readonly T[]), renders
role="listbox" with aria-activedescendant, and keeps DOM focus on itself.
listbox-grid-item.vue renders role="option" and registers with the shared
collection registry, which also powers buffered typeahead. Arrow keys follow
the visual grid through the pure moveInGrid helper; columns must match the
consumer's CSS grid. Named grids submit one hidden input per selected value.
No CSS ships.
Normative Behavior
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| G1 | any | render | role="listbox", tabindex=0, data-columns, options expose row/column slot state, hidden form inputs |
renders listbox semantics with row and column slot state |
| G2 | focused | Arrow keys / Home / End / Ctrl+Home/End / PageUp / PageDown | focus highlights the selection, then moves in two dimensions, skipping disabled options; edges hold | focus highlights the selected option, then arrows move in two dimensions |
| G3 | dir="rtl" |
ArrowLeft / ArrowRight | mirrored | rtl mirrors horizontal arrows |
| G4 | single | Enter / Space / selectionFollowsFocus |
selects the highlighted option; change carries the key event; follow-focus selects on arrows |
Enter and Space select in single mode; selectionFollowsFocus selects on arrows |
| G5 | multiple | Space / Shift+Arrow / Ctrl+A | toggles, extends, selects every enabled option | multiple mode toggles, extends with Shift+arrows, and selects all with Ctrl+A |
| G6 | any | pointer | pointer-down keeps focus on the grid; click selects; disabled options ignore input | click selects; disabled options ignore pointer input |
| G7 | focused | printable character | typeahead moves the highlight by textValue |
typeahead moves the highlight by option text |
| G8 | disabled | Tab / keys | removed from the tab order, keys ignored | disabled grids leave the tab order and ignore keys |
| G9 | controlled | click | emits and waits for the parent; by matches structurally equal values |
controlled values wait for the parent |
| G10 | no provider | mount an item | throws VIZE_UI_CONTEXT_MISSING |
items require a ListboxGrid provider |
| G11 | pure helper | moveInGrid |
empty grids, ragged last rows, page moves, and row-wrap opt-out are deterministic | pure grid navigation handles empty grids, ragged rows, and page moves |
SSR
Ids come from the deterministic-id primitive; server markup is byte-identical
across isolated requests and hydrates without warnings (renders byte-identical ListboxGrid markup across isolated SSR requests, hydrates ListboxGrid without mismatches or node replacement).