Selectors
Composite picker patterns built on top of Dropdown Menu. These are concrete UX templates — copy the structure when you need a similar shape, then adapt the data, icons, and copy.
Patterns
Model selector (tier picks + submenu of specific models)
Two top-level "tier" picks (Auto, Fast) with inline descriptions, then a More models submenu housing the specific provider models. The sub-trigger shows the active model name on the right when a model from the submenu is selected, so the user can see the current pick without diving in. Mirrors the real ChatModeSelector in apps/frontend.
Datasource selector (search + groups + switches)
Multi-toggle picker with fuzzy search across name and description, sources grouped by External / Internal, and a Switch per row. Clicking the row or the switch flips state; closeOnClick={false} keeps the menu open so users can toggle several sources without reopening. Empty-state copy ("No sources found") renders when the query filters out everything.
Settings toggles (icon + switch)
The simplest end of this family — single-row preference toggles with an icon on the left, a switch on the right, and a separator splitting groups (Notifications, Privacy). Use this shape when the settings live inside a quick-access menu rather than a full settings page.
When to use each shape
| Shape | When |
|---|---|
| Model selector | One-of-many pick where the options have meaningful differences. Use submenus when the long tail is large but a few tiers cover most cases. |
| Datasource selector | Multi-toggle where each option is independent and the list can grow beyond ~8 items. Always include search once the list crosses ~10. |
| Settings toggles | A handful of independent on/off prefs that need immediate effect. If you have more than ~6, move them into a settings page instead. |
Recipes
Surface the deep-nested selection on the parent
When a submenu houses options the user picks rarely, show the active pick as a tertiary hint on the sub-trigger so the parent menu still communicates state at a glance.
<DropdownMenuSubTrigger>
<span className="grow">More models</span>
{isMoreModelSelected && (
<span className="text-tertiary mr-1 text-xs">{selected.name}</span>
)}
</DropdownMenuSubTrigger>Search that filters across multiple fields
DropdownMenuSearch doesn't ship with built-in filtering — you bring the logic. For richer datasets, configure Fuse with keys so the search spans more than just the visible label:
const FUSE_OPTIONS = {
threshold: 0.4,
ignoreLocation: true,
keys: ["name", "description"],
} as const;
const fuse = useMemo(() => new Fuse(items, FUSE_OPTIONS), [items]);
const filtered = useMemo(() => {
const q = query.trim();
if (q === "") return items;
return fuse.search(q).map((r) => r.item);
}, [fuse, query, items]);Switch row that toggles via row click OR switch click
Wire onSelect on the row AND onCheckedChange on the switch to the same setter. closeOnClick={false} keeps the menu open so the user can flip several:
<DropdownMenuItem
closeOnClick={false}
onSelect={() => onCheckedChange(!checked)}
prefix={<Icon className="text-tertiary" />}
suffix={<Switch checked={checked} onCheckedChange={onCheckedChange} aria-label={name} />}
>
<div className="flex min-w-0 flex-col leading-tight">
<span className="text-primary truncate">{name}</span>
<span className="text-tertiary truncate text-xs">{description}</span>
</div>
</DropdownMenuItem>Group + empty state under search
When you split filtered results into groups, hide the group labels when their bucket is empty so the menu doesn't show a bare label with nothing under it. Show a single empty state if every group is empty.
{filtered.length === 0 ? (
<div className="text-tertiary px-2 py-6 text-center text-sm">No sources found</div>
) : (
<>
{external.length > 0 && (
<>
<DropdownMenuLabel>External</DropdownMenuLabel>
{external.map(renderRow)}
</>
)}
{external.length > 0 && internal.length > 0 && <DropdownMenuSeparator />}
{internal.length > 0 && (
<>
<DropdownMenuLabel>Internal</DropdownMenuLabel>
{internal.map(renderRow)}
</>
)}
</>
)}See also
- Dropdown Menu — the primitive these patterns are built on.
- Switch — the right-slot control used in the toggle-based selectors.