ProductBrand
Join us
Foundations
  • Introduction
  • Colors
  • Typography
  • Icons
  • Illustrations
  • Logos
  • Shadows
Components
  • Accordion
  • Alert Dialog
  • Avatar
  • Badge
  • Button
  • Calendar
  • Card
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Context Menu
  • Date Picker
  • Dialog
  • Drawer
  • Dropdown Menu
  • Fluid Avatar
  • Hover Scroll Text
  • Input
  • Kbd
  • Popover
  • Progress Bar
  • Radio Group
  • Scroll Area
  • Select
  • Selectors
  • Separator
  • Separator Dot
  • Shimmer Text
  • Skeleton
  • Slider
  • Switch
  • Tabs
  • Textarea
  • Toast
  • Tooltip
Compositions
  • Empty State
  • File Attachment
  • Floating Bar
  • Input Banner
  • Markdown Editor
  • Media Controls
  • Product Feature Banner
  • Panels
  • Sidebar
  • Reasoning Trace
  • Pagination
  • Data Table
  • Breadcrumb
  • Thread Outline
Inline
  • Overview
  • Stat Tile
  • Metric Row
  • Comparison Table
  • Chart Card
  • Trend Tile
  • Cite
  • Custom
  • Agent skill

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

A configurable datasource-style selector. Search filters across name and description (see the Fuse recipe below for fuzzy matching), External / Internal group labels split the list, and each row toggles independently via a Switch (or the built-in checkmark). Clicking the row or the switch flips state; closeOnClick={false} keeps the menu open so users can toggle several sources without reopening, and "No sources found" renders when the query filters out everything.

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.

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

ShapeWhen
Model selectorOne-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 selectorMulti-toggle where each option is independent and the list can grow beyond ~8 items. Always include search once the list crosses ~10.
Settings togglesA 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.

tsx
<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, so you bring the logic. For richer datasets, configure Fuse with keys so the search spans more than just the visible label:

tsx
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:

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

tsx
{
  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.
PreviousSelect
NextSeparator
Made in NYC© 2026 Rogo Technologies Inc.

Design at Rogo

We’re redesigning an entire industry. Come design it with us.

See open roles

Design at Rogo

  • How we workThe mission of design at Rogo, and the open roles
  • About RogoThe company and the product