npm.io
1.0.0-next.1 • Published 2d ago

@solid-primitives/list-state

Licence
MIT
Version
1.0.0-next.1
Deps
1
Size
31 kB
Vulns
0
Weekly
0
Stars
1.5K

Solid Primitives list-state

@solid-primitives/list-state

size version stage tested with vitest

Primitives for managing keyboard-navigable list state. Provides accessible, fully-featured list state management with support for single-select and multi-select patterns.

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

Keywords