Solid List Utility
solid-list is a SolidJS utility designed to simplify the creation of accessible and keyboard-navigable lists, suitable for components like search results, custom selects, or autocompletes. It supports both vertical and horizontal orientations, single and multi-selection patterns, and integrates with `rtl` and `ltr` text directions. The library is unopinionated about styling and works with various list implementations, including virtualized lists, by providing utilities rather than prescriptive components. Key features include optional looping behavior, a 'Vim mode' for navigation, and configurable tab key handling. It is currently at version 0.3.0 and is part of the `corvu` collection of unstyled UI primitives for SolidJS. This suggests active development and a release cadence aligned with other `corvu` packages, which generally see updates every few weeks or months. Its primary differentiator is its strong focus on accessibility and keyboard navigation capabilities without imposing any specific UI or styling opinions on the developer.
Common errors
-
TypeError: createList is not a function
cause Attempting to import `createList` using CommonJS `require()` syntax in an environment that expects ESM, or incorrect import path.fixEnsure you are using ES module import syntax: `import { createList } from 'solid-list'` and that your build setup supports ESM. -
Keyboard navigation does not work or active item jumps erratically.
cause The `onKeyDown` handler from `createList` is not attached to the correct element, or the `items` accessor is not reactive, or list items lack unique, stable `id`s.fixAttach `onKeyDown` to the parent container of your list or the input that triggers list interaction. Verify that the `items` option passed to `createList` is a reactive signal/memo returning unique IDs, and that your rendered list items have consistent `id` attributes that match these IDs.
Warnings
- gotcha The documentation for the `handleTab` option in the README is contradictory. One line states `handleTab: false, // default = true`. It is safer to explicitly set `handleTab` to your desired behavior (`true` or `false`) rather than relying on an ambiguous default.
- breaking `solid-list` is currently in a pre-1.0 version (0.3.0). As such, breaking changes may be introduced in minor releases without strict adherence to semantic versioning until a stable 1.0 release. Always review changelogs when upgrading.
- gotcha The `items` option for `createList` must be a function that returns an array of unique identifiers for your list items (e.g., `() => results().map(result => result.id)`). If the accessor is not reactive or does not return stable IDs, keyboard navigation or active state management might behave unpredictably.
Install
-
npm install solid-list -
yarn add solid-list -
pnpm add solid-list
Imports
- createList
const createList = require('solid-list')import { createList } from 'solid-list'
Quickstart
import { createSignal, For } from 'solid-js';
import { createList } from 'solid-list';
const Search = () => {
const [results, setResults] = createSignal([
{ id: '1', name: 'Apple', href: '/products/apple' },
{ id: '2', name: 'Banana', href: '/products/banana' },
{ id: '3', name: 'Cherry', href: '/products/cherry' },
{ id: '4', name: 'Date', href: '/products/date' }
]);
const { active, setActive, onKeyDown } = createList({
items: () => results().map(result => result.id), // required, should be a reactive accessor
initialActive: null, // default, T | null
orientation: 'vertical', // default, 'vertical' | 'horizontal'
loop: true, // default
textDirection: 'ltr', // default, 'ltr' | 'rtl'
handleTab: true, // default
vimMode: false, // default
onActiveChange: (newActiveId) => {
console.log('Active item changed to:', newActiveId);
// Optionally, scroll into view or navigate
}
});
const handleInput = (e) => {
const query = e.target.value.toLowerCase();
setResults([
{ id: '1', name: 'Apple', href: '/products/apple' },
{ id: '2', name: 'Banana', href: '/products/banana' },
{ id: '3', name: 'Cherry', href: '/products/cherry' },
{ id: '4', name: 'Date', href: '/products/date' }
].filter(item => item.name.toLowerCase().includes(query)));
};
return (
<div class="search-container">
<input
type="text"
placeholder="Search fruits..."
onInput={handleInput}
onKeyDown={onKeyDown}
aria-controls="search-results-list"
aria-expanded={results().length > 0}
role="combobox"
/>
{results().length > 0 && (
<ul id="search-results-list" role="listbox">
<For each={results()}>
{(item) => (
<li
id={`item-${item.id}`}
role="option"
aria-selected={active() === item.id}
tabIndex={active() === item.id ? 0 : -1}
onClick={() => setActive(item.id)}
onMouseEnter={() => setActive(item.id)} // for mouse navigation
>
<a href={item.href} tabIndex={-1}>{item.name}</a>
</li>
)}
</For>
</ul>
)}
</div>
);
};