Keyboard navigable list state management primitives
| Stage | Category | Version | Last Updated | Demo |
|---|---|---|---|---|
| 3 | Inputs | 1.0.0-next.2 (next) | Aug 12, 2026 | Demo → |
npm i @solid-primitives/list-state@nextPrimitives for managing keyboard-navigable list state. Provides accessible, fully-featured list state management with support for single-select and multi-select patterns.
createListState— Single-select list with keyboard navigationcreateMultiSelectListState— Multi-select list with cursor-based navigation and range selection
Features
- Full keyboard support: Arrow keys, Vim bindings (hjkl), Home/End, Tab
- Configurable orientation: Vertical or horizontal lists
- Bidirectional text: RTL and LTR support
- List looping: Optional looping at list boundaries
- Range selection: Shift+Arrow for multi-select range expansion
- Type-safe: Full TypeScript support with generic item types
- SSR-safe: Works in both browser and server environments
- Zero dependencies: Relies only on Solid.js primitives
createListState
A reactive primitive for single-select list navigation. Returns an active signal for the currently selected item and an onKeyDown handler for keyboard events.
import { createListState } from "@solid-primitives/list-state";import { createSignal } from "solid-js";
export function MyList() { const items = ["Apple", "Banana", "Cherry"]; const { active, setActive, onKeyDown } = createListState({ items, initialActive: items[0], });
return ( <ul role="listbox" onKeyDown={onKeyDown}> <For each={items}> {(item) => ( <li role="option" aria-selected={active() === item} onClick={() => setActive(item)} class={{ selected: active() === item }} > {item} </li> )} </For> </ul> );}Props
interface ListStateOptions<T> { /** The items in the list. Should be in the same order as they appear in the DOM. */ items: MaybeAccessor<T[]>;
/** The initially active item. @default undefined */ initialActive?: T | undefined;
/** The orientation of the list. @default "vertical" */ orientation?: MaybeAccessor<"vertical" | "horizontal">;
/** Whether the list should loop. @default true */ loop?: MaybeAccessor<boolean>;
/** The text direction of the list. @default "ltr" */ textDirection?: MaybeAccessor<"ltr" | "rtl">;
/** Whether tab key presses should be handled. @default true */ handleTab?: MaybeAccessor<boolean>;
/** Whether vim movement key bindings should be used. @default false */ vimMode?: MaybeAccessor<boolean>;
/** The vim movement key bindings. @default { up: 'k', down: 'j', right: 'l', left: 'h' } */ vimKeys?: MaybeAccessor<{ up: string; down: string; right: string; left: string }>;
/** Callback fired when the active item changes. */ onActiveChange?: (active: T | undefined) => void;}Returns
interface ListStateReturn<T> { active: Accessor<T | undefined>; setActive: (value: T | undefined) => void; onKeyDown: (event: KeyboardEvent) => void;}Keyboard Shortcuts
| Key | Action |
|---|---|
| ↑ / ↓ | Navigate vertically (or ← / → for horizontal) |
| Home | Jump to first item |
| End | Jump to last item |
| Tab | Navigate forward (if handleTab is true) |
| Shift+Tab | Navigate backward (if handleTab is true) |
| k / j | Vim navigation (if vimMode is true) |
createMultiSelectListState
A reactive primitive for multi-select list navigation with range selection. Maintains three separate states: cursor (focused item), active (range of focused items), and selected (user-selected items).
import { createMultiSelectListState } from "@solid-primitives/list-state";
export function MyMultiSelectList() { const items = ["Apple", "Banana", "Cherry"]; const { cursor, active, selected, setCursorActive, toggleSelected, onKeyDown } = createMultiSelectListState({ items, });
return ( <ul role="listbox" onKeyDown={onKeyDown}> <For each={items}> {(item) => ( <li role="option" aria-selected={selected().includes(item)} onClick={() => setCursorActive(item)} onDoubleClick={() => toggleSelected(item)} class={{ cursor: cursor() === item, selected: selected().includes(item), }} > {item} </li> )} </For> </ul> );}Props
interface MultiSelectListStateOptions<T> { // Same as ListStateOptions<T>, plus:
/** The initially focused item (cursor). @default undefined */ initialCursor?: T | undefined;
/** The initially active items (range). @default [] */ initialActive?: T[];
/** The initially selected items. @default [] */ initialSelected?: T[];
/** Callback fired when the cursor changes. */ onCursorChange?: (cursor: T | undefined) => void;
/** Callback fired when the active items change. */ onActiveChange?: (active: T[]) => void;
/** Callback fired when the selected items change. */ onSelectedChange?: (selected: T[]) => void;}Returns
interface MultiSelectListStateReturn<T> { cursor: Accessor<T | undefined>; setCursor: (value: T | undefined) => void; active: Accessor<T[]>; setActive: (value: T[]) => void; setCursorActive: (item: T | undefined) => void; selected: Accessor<T[]>; setSelected: (value: T[]) => void; toggleSelected: (item: T) => void; onKeyDown: (event: KeyboardEvent) => void;}Keyboard Shortcuts
All shortcuts from createListState plus:
| Key | Action |
|---|---|
| Shift+↑ / ↓ | Expand/contract selection range |
Types
export type Orientation = "vertical" | "horizontal";export type TextDirection = "ltr" | "rtl";
export type VimKeys = { up: string; down: string; right: string; left: string;};Server-Side Rendering
Both primitives are fully SSR-safe and will work correctly in both browser and server environments.
Credits
This primitive was adapted from corvu's solid-list by Jasmin Noetzli and migrated to Solid Primitives for Solid 2.0. Used under the MIT License.
Changelog
See CHANGELOG.md