@solid-primitives/list-state
Primitives 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
Installation
npm install @solid-primitives/list-state
# or
yarn add @solid-primitives/list-state
# or
pnpm add @solid-primitives/list-state
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