Grid List
Accessible, unstyled GridList (WAI-ARIA APG grid pattern for interactive lists): roving focus, typeahead, single/multiple selection with ranges, list or 2D grid layouts, and pointer + keyboard drag reordering.
| Package | @vizejs/ui/grid-list |
| Maturity | stable |
| Own the source | vize lib pull grid-list |
| Requires | collection, context, controllable-state, drag-and-drop, id, sortable, typeahead |
| Aliases | selectable list, reorderable list, card grid, gallery grid |
| Covers | WAI-ARIA grid pattern, React Aria GridList, Reka UI Listbox |
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 { GridList, GridListItem } from "@vizejs/ui/grid-list";
Or copy the source into your project with vize lib pull grid-list (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
<!-- Multi-select, reorderable task list bound with v-model for both items and selection. -->
<script setup lang="ts">
import { ref } from "vue";
import { GridList } from "@vizejs/ui/grid-list";
interface Task {
readonly id: string;
readonly title: string;
readonly due: string;
}
const tasks = ref<readonly Task[]>([
{ id: "draft", title: "Draft release notes", due: "Mon" },
{ id: "review", title: "Review pull requests", due: "Tue" },
{ id: "deploy", title: "Deploy to staging", due: "Wed" },
{ id: "retro", title: "Run the retrospective", due: "Fri" },
]);
const selection = ref<readonly string[]>(["review"]);
const taskKey = (task: Task): string => task.id;
const taskText = (task: Task): string => task.title;
</script>
<template>
<div>
<GridList
v-model:items="tasks"
v-model:selection="selection"
:get-key="taskKey"
:get-text-value="taskText"
selection-mode="multiple"
:disabled-keys="['retro']"
reorderable
aria-label="Tasks this week"
>
<template #item="{ item, selected }">
<span>{{ item.title }}</span>
<span>due {{ item.due }}</span>
<span v-if="selected">(selected)</span>
</template>
</GridList>
<output>{{ selection.length }} of {{ tasks.length }} tasks selected</output>
</div>
</template>
API
GridList<Item>
Source: src/families/data/grid-list/grid-list.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items (required) |
readonly Item[] |
undefined |
Items rendered as rows. |
getKey |
(item: Item, index: number) => string |
undefined |
Stable key per item; defaults to the index. |
getTextValue |
(item: Item) => string |
undefined |
Typeahead text per item; defaults to the rendered text. |
id |
string | null |
undefined |
Consumer-owned list id. null and undefined select a deterministic fallback. |
selectionMode |
GridListSelectionMode |
"single" |
Selection policy. |
selection |
readonly string[] |
undefined |
Controlled selected keys (v-model:selection). |
defaultSelection |
readonly string[] |
[] |
Initial uncontrolled selection. |
disabledKeys |
readonly string[] |
[] |
Keys that stay focusable but cannot be selected, activated, or dragged. |
layout |
GridListLayout |
"list" |
list moves with Up/Down; grid also moves with Left/Right and steps Up/Down by columns. |
columns |
number |
1 |
Items per visual row in the grid layout. |
dir |
"ltr" | "rtl" |
"ltr" |
Reading direction for Left/Right in the grid layout. |
loop |
boolean |
false |
Wrap arrow navigation at both ends. |
reorderable |
boolean |
false |
Allow pointer and keyboard reordering through the slot's dragHandleProps. |
ariaLabel |
string |
undefined |
Accessible name of the grid. |
ariaLabelledby |
string |
undefined |
Ids that label the grid. |
Events
| Event | Payload | Description |
|---|---|---|
update:selection |
[value: readonly string[]] |
Fired with the new selected keys (supports v-model:selection). |
action |
[item: Item, key: string] |
Fired when Enter or a double click activates an enabled item. |
update:items |
[value: readonly Item[]] |
Fired with the reordered items (supports v-model:items). |
reorder |
[event: GridListReorderEvent<Item>] |
Fired after a reorder commits. |
Slots
| Slot | Slot props | Description |
|---|---|---|
item |
GridListItemSlotProps<Item> |
Item contents. |
empty |
— | Rendered when items is empty. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLDivElement | null |
Rendered role="grid" element. |
focus |
(key?: string) => void |
Focus an item by key (or the active/first item). |
selectAll |
() => boolean |
Select every enabled item (multiple selection only). |
clearSelection |
() => boolean |
Clear the selection. |
GridListItem
Source: src/families/data/grid-list/grid-list-item.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
itemKey (required) |
string |
undefined |
Stable item key (registry key and DOM data hook). |
index (required) |
number |
undefined |
Zero-based item index, published as aria-rowindex (one-based). |
selected |
boolean |
false |
Whether the item is selected. |
disabled |
boolean |
false |
Whether the item is disabled (focusable, not selectable or draggable). |
textValue |
string |
undefined |
Typeahead text; rendered text is used when omitted. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly active: boolean; readonly dragging: boolean; readonly dragHandleProps: Readonly<DragSourceProps> | null; } |
Item contents. Receives drag state and handle props. |
Behavior
@vizejs/ui/grid-list applies the WAI-ARIA APG grid pattern to interactive lists. grid-list.vue is the
generic root (Item); grid-list-item.vue renders each role="row" with one role="gridcell". Reordering
uses the shared sortable foundation through a drag handle. Every row is proven by the named test in
grid-list.test.ts or grid-list-ssr.test.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| L1 | any | render | role="grid" with aria-rowcount, rows with aria-rowindex and one gridcell, a single tab stop |
renders an APG grid of rows with one gridcell and a single tab stop |
| L2 | layout="list" |
Up/Down, Home/End, PageUp/PageDown, loop, characters |
roving focus moves, wraps with loop, and typeahead matches getTextValue |
list layout: Up/Down, Home/End, PageUp/PageDown, loop, and typeahead |
| L3 | layout="grid" |
Left/Right, Up/Down, dir="rtl" |
Left/Right step items, Up/Down step by columns, rtl flips |
grid layout: Left/Right step items, Up/Down step rows, rtl flips |
| L4 | selectionMode="multiple" |
click, Ctrl, Shift, Space, Shift+Arrow, Ctrl+A | replace, toggle, and range selection skipping disabled items; aria-selected / aria-disabled |
multiple selection: click, Ctrl-click, Shift-click, Space, Shift+Arrow, Ctrl+A |
| L5 | single / none | click, Enter, double click | single replaces; none publishes no aria-selected; Enter and double click emit action |
single selection replaces; none mode publishes no aria-selected; Enter and double click act |
| L6 | reorderable |
Enter on handle, arrows, Enter | a keyboard move commits; reorder and update:items fire; list keys ignore handle events |
keyboard reorder through the drag handle commits and emits the new order |
| L7 | no items | render | an empty row renders the empty slot |
empty lists render an empty row |
| L8 | custom compositions | import | GridListItem is exported |
GridListItem is exported for custom compositions |
| L9 | SSR | two requests + hydrate | byte-identical markup with one tab stop; warning-free hydration | renders identical grid-list markup across SSR requests and hydrates cleanly |