The problem
Long flat lists are hard to scan. When your options have natural categories (food by type, users by department, tools by platform), grouping them with headers makes the dropdown far more usable. But static groups break the moment search filtering kicks in: if no items in a category match the query, you get an empty group header sitting there looking awkward.
The grouping system in this library is dynamic. You provide a function that computes groups from the currently filtered options. When the user searches, groups automatically recalculate. Empty categories disappear. The group headers always reflect what's actually visible.
How it works
Two props work together: handleGroups computes group boundaries from the
current filtered options, and groupContent renders each group header.
import { useState } from "react";
import { SearchableDropdown } from "@luciodale/react-searchable-dropdown";
import "@luciodale/react-searchable-dropdown/dist/single-style.css";
type Food = {
label: string;
value: string;
category: string;
};
const foods: Food[] = [
{ label: "Chicken", value: "chicken", category: "meat" },
{ label: "Turkey", value: "turkey", category: "meat" },
{ label: "Carrots", value: "carrots", category: "veggies" },
{ label: "Broccoli", value: "broccoli", category: "veggies" },
{ label: "Salmon", value: "salmon", category: "fish" },
{ label: "Tuna", value: "tuna", category: "fish" },
{ label: "Apple", value: "apple", category: "fruit" },
{ label: "Banana", value: "banana", category: "fruit" },
];
function handleGroups(matchingOptions: Food[]) {
const grouped = matchingOptions.reduce(
(acc, item) => {
const key = item.category || "Other";
acc[key] = (acc[key] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
return {
groupCategories: Object.keys(grouped),
groupCounts: Object.values(grouped),
};
}
function FoodPicker() {
const [food, setFood] = useState<Food | undefined>(undefined);
return (
<SearchableDropdown
options={foods}
value={food}
setValue={setFood}
searchOptionKeys={["label"]}
handleGroups={handleGroups}
groupContent={(index, categories) => (
<div className="lda-dropdown-group">{categories[index]}</div>
)}
placeholder="Pick a food..."
/>
);
}The handleGroups function
handleGroups receives the filtered options (after search) and returns two
parallel arrays:
groupCategories: names for each group (displayed in headers)groupCounts: how many items belong to each group
The order matters. Items in the options array must be sorted so that items in the same group are adjacent. The counts tell the virtualizer where each group starts and ends.
Multi-select with groups
Grouping works identically in SearchableDropdownMulti. Pass the same
handleGroups and groupContent props.
function FoodPickerMulti() {
const [foods, setFoods] = useState<Food[] | undefined>(undefined);
return (
<SearchableDropdownMulti
options={allFoods}
values={foods}
setValues={setFoods}
searchOptionKeys={["label"]}
handleGroups={handleGroups}
groupContent={(index, categories) => (
<div className="lda-multi-dropdown-group">
{categories[index]}
</div>
)}
placeholder="Pick foods..."
/>
);
}Context prop
The context prop passes arbitrary data to groupContent. This
is useful when group headers need access to callbacks, configuration, or external state
without closing over component scope.
<SearchableDropdown
options={foods}
value={food}
setValue={setFood}
searchOptionKeys={["label"]}
context={{ highlightColor: "#ef4723" }}
handleGroups={handleGroups}
groupContent={(index, categories, ctx) => (
<div style={{ color: ctx.highlightColor }}>
{categories[index]}
</div>
)}
/>Next steps
- API Reference — complete type definitions including TGroups
- Live demo — grouped dropdowns in action
- Styling — customize group header appearance