A headless option selection library for JavaScript front-end frameworks like React and Nextjs (and more later). It simplifies building hierarchical selectors, multi-select dropdowns, and tree-style selectors — while giving you full control over UI.
Live demo : option-select-demo
![]() |
![]() |
|---|---|
| Option-Select Demo (hierarchical selectors) | Selection Limit Demo |
- ✅ Headless: Bring your own UI (works with React, Nextjs).
- ✅ Hierarchical Selection: Handles nested data with parent-child relationships.
- ✅ Selection Limit: Restrict how many root-level items can be selected with a
limitprop. - ✅ Select all & deselect all functionality
- ✅ Retrieve selected items on demand or on every selection changes happen
- ✅ Accept isSelected for default selection, but if an item has subItems and not all subItems have isSelected: true, the parent’s isSelected state will be ignored.
npm install option-selector
yarn add option-selectimport { useCallback } from "react";
import { useOptionSelect, OptionItem } from "option-select";
interface ItemProps {
id: string;
name: string;
age?: number;
isSelected?: boolean;
subItems?: ItemProps[];
}
const items: ItemProps[] = [
{
id: "1",
name: "Parent 1",
subItems: [
{ id: "1-1", name: "Child 1-1", isSelected: true },
{ id: "1-2", name: "Child 1-2", isSelected: true },
],
},
{
id: "2",
name: "Parent 2",
subItems: [
{
id: "2-1",
name: "Child 2-1",
isSelected: true,
subItems: [{ id: "2-1-1", name: "Child 2-1-1" }],
},
{
id: "2-2",
name: "Child 2-2",
subItems: [{ id: "2-2-1", name: "Child 2-2-1" }],
},
],
},
];
const Demo = () => {
const { getAllItems, selectAll, deselectAll, getSelectedItems } = useOptionSelect(
{
items: items,
getId: (item) => item.id,
onSelectionChange: handleOnChange,
limit: 1 // 🔥 Only allow one root-level item to be selected
}
);
const handleOnChange = useCallback((items) => {
console.log("selected items", items);
},[])
// render item and it's sub items Recursively
function renderItem({ item, isSelected, toggleSelection, subItems }: OptionItem<ItemProps>) {
return (
<div key={item.id}>
<label>
<input type="checkbox" checked={isSelected} onChange={toggleSelection} />
{item.name}
</label>
{subItems && (
<div style={{ paddingLeft: 20 }}>
{subItems.map((subItem) => renderItem(subItem))}
</div>
)}
</div>
);
}
return (
<div>
<button onClick={selectAll}>Select All</button>
<button onClick={deselectAll}>Deselect All</button>
<button onClick={() => console.log("Selected Items:", getSelectedItems())}>
Get Selected Items
</button>
{getAllItems().map((item) => renderItem(item))}
</div>
);
}useOptionSelect({items: T[], getId: (item: T) => string, onSelectionChange?: (selectedItems: T[]) => void})
uesOptionSlect hook accept three props
- items
- Selection item array, each with isSelected ( default is false ) and subItems (optional) fields.
- getId
- Unique id selector function.
- onSelectionChange
(optional)- Callback function you want to call on every selection changes.
- limit
(optional)- Maximum number of root-level items that can be selected.
Sub-items are not affected by this limit.
- Maximum number of root-level items that can be selected.
useOptionSelect returns an object with the following methods:
getAllItems(): { item: T; isSelected: boolean; toggleSelection: () => void; subItems?: T[] }[]- Returns items with it's corresponding sub items wrapped with selection controls (isSelected, toggleSelection).
selectAll(): void- Selects all items.
deselectAll(): void- Deselects all items.
getSelectedItems(): T[]- Returns selected items in the same hierarchical structure as the input.
Contributions are welcome!
-
Fork the repo and create your branch (git checkout -b feature/amazing-feature).
-
Install dependencies with yarn install.
-
Open a Pull Request.
See CONTRIBUTING.md for more details.
MIT

