esc

Type to search...

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.

GroupedDropdown.tsx
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.

GroupedMultiDropdown.tsx
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.

ContextExample.tsx
<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