Components · Inputs
Combobox
BetaSearches and picks from a long list, such as the exercise library.
- Source
components/ui/command.tsx - shadcn/ui Combobox
- Figma: not linked yet (Q13)
When to use
- To pick one option from a long list where typing is faster than scrolling: an exercise, a food, a client, a program template.
- When coaches know roughly what they're looking for and can type part of the name ("dead", "row").
When not to use
- About 5 to 15 options: use a Select. It needs no typing and works the same on every device.
- 2 to 4 options that should all be visible: use a Radio group.
- To run actions or jump to pages: use a Command menu.
- To browse the exercise library with filters, video previews and multi-select: that is the Exercise picker, which builds on this.
Anatomy
- 1. Label: always visible, above the trigger
- 2. Trigger: an outline button with role combobox, showing the chosen value or a placeholder
- 3. Chevron: shows that the trigger opens a list
A combobox is a composition, not a single component. It is a Popover whose content is a Command, as in shadcn's combobox recipe. Open the example above to see the popover's parts: a search field at the top, group headings ("Lower body"), options with a check on the chosen one, and an empty message when nothing matches. The popover is as wide as the trigger.
Variants
One style. Adapt it with what the list needs:
- Grouped:
CommandGroupwith aheading, as in the example. Use groups only when coaches think in those groups (muscle area, meal type). - Single choice: choosing an option closes the popover and puts its label in the trigger. Choosing the same option again clears it.
- Several choices: keep the popover open on select and show the choices as badges under the trigger. This is how the Exercise picker adds several exercises at once.
States
Choose an exercise for this block.
| State | What changes | Token |
|---|---|---|
| Hover (trigger) | Accent fill | bg-accent, text-accent-foreground |
| Focus-visible (trigger) | Border and a 3px ring | border-ring, ring-ring/50 |
| Open | Popover with border and shadow, on the popover layer | bg-popover, shadow-md, z-(--z-popover) |
| Highlighted option | Accent fill, moved by arrows or the pointer | bg-accent, text-accent-foreground |
| Placeholder | Lighter text in the trigger and search field | text-muted-foreground |
| No results | Centered empty message | text-sm |
| Disabled (trigger or option) | Half opacity, no pointer events | opacity-(--opacity-disabled) |
| Invalid | Red border and ring, error text below | border-destructive, ring-destructive/20, text-destructive-text |
Usage guidelines
- Match anywhere in the name, not only the start: "dead" finds "Romanian deadlift".
cmdkdoes this by default. - Search the label the coach sees. Pass the visible label as the item's
value, so English and Arabic names are both searchable in their own language. - Keep the chosen value visible in the trigger. Don't clear it when the popover opens.
- Lists longer than a few hundred items should load results as the coach types, with a loading line in the list.
Content guidelines
- Label: a short noun, sentence case: "Exercise", "Food", "Assign to client".
- Trigger placeholder: an instruction, "Choose an exercise". Search placeholder: "Search exercises…", with an ellipsis.
- Options: the name as coaches say it ("Barbell back squat"), not codes or IDs. Put detail such as equipment in a second line, not in brackets.
- Empty message: say what happened and what to do: "No exercise matches. Check the spelling."
- Arabic exercise names are the ones Gulf coaches use in the gym. Keep common English loan words such as "سكوات" where that is what coaches say.
Accessibility
| Key | Does |
|---|---|
| Enter, Space (on the trigger) | Opens the popover and moves focus to the search field |
| Type | Filters the list; the first match is highlighted |
| Arrow down / up | Moves the highlight through options, skipping disabled ones |
| Home / End | Moves to the first or last option |
| Enter | Chooses the highlighted option and closes the popover |
| Escape | Closes without changing the value; focus returns to the trigger |
| Tab | Closes the popover and moves on |
- The trigger is a button with
role="combobox"andaria-expanded. It is labelled by the label and its current value (aria-labelledbylists both ids), so a screen reader announces "Exercise Romanian deadlift, combobox, collapsed". - The search field is a
comboboxwitharia-autocomplete="list", controlling alistboxofoptions. Name the field withCommand'slabelprop and the list withCommandList'slabelprop; without them, the field has no name and the list is called "Suggestions" in English, also on Arabic screens. - The highlighted option is set with
aria-activedescendant, so focus stays in the search field while the screen reader reads each highlighted option: "Romanian deadlift". aria-selectedmarks the highlighted option, not the chosen one; the chosen value is announced from the trigger.- The empty message is not a live region, so screen readers don't announce it on their own. Keep it short so it is quick to find with the reading cursor.
RTL
- The chevron sits at the end of the trigger and the check at the end of each option (
ms-auto), so both move to the left. - The popover aligns to the trigger's start edge (
align="start"), which is its right edge in RTL. - The search icon stays at the start of the field, on the right.
- Typing Arabic filters Arabic labels; Latin brand names inside Arabic labels stay left to right on their own.
Tokens used
Read from components/ui/command.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.
| Token | Utilities | SuperCoach | Coach |
|---|---|---|---|
| --accent | bg-accent | brand-50 | orange-50 |
| --accent-foreground | text-accent-foreground | brand-700 | orange-700 |
| --border | bg-border | neutral-200 | neutral-200 |
| --foreground | text-foreground | neutral-950 | neutral-950 |
| --muted-foreground | text-muted-foreground | neutral-600 | neutral-600 |
| --popover | bg-popover | white | white |
| --popover-foreground | text-popover-foreground | neutral-950 | neutral-950 |
- Opacity
opacity-(--opacity-disabled) - Radius
rounded-md - Radius
rounded-sm - Type
font-medium - Type
text-sm - Type
tracking-wide - Type
text-xs
Popover
Read from components/ui/popover.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.
| Token | Utilities | SuperCoach | Coach |
|---|---|---|---|
| --muted-foreground | text-muted-foreground | neutral-600 | neutral-600 |
| --popover | bg-popover | white | white |
| --popover-foreground | text-popover-foreground | neutral-950 | neutral-950 |
- Layer
z-(--z-popover) - Radius
rounded-md - Shadow
shadow-md - Type
font-medium - Type
text-sm
Props
The combobox has no component of its own: these are the Command parts it uses. See Popover for Popover, PopoverTrigger and PopoverContent.
<Command>
Also takes every prop of cmdk Command
No props of its own.
<CommandInput>
Also takes every prop of cmdk Command.Input
No props of its own.
<CommandList>
Also takes every prop of cmdk Command.List
No props of its own.
<CommandEmpty>
Also takes every prop of cmdk Command.Empty
No props of its own.
<CommandGroup>
Also takes every prop of cmdk Command.Group
No props of its own.
<CommandItem>
Also takes every prop of cmdk Command.Item
No props of its own.
Replaces in coach-admin
From the read-only audit of coach-admin: 53 style values, 71 uses.
coach-admin uses react-widgets (rw- classes: DropdownList, Multiselect) and small custom search inputs. Searchable lists become this combobox; short ones become a Select. The exercise library panel is documented separately as the Exercise picker.
Classes and selectors
.rw-widget-container17.rw-widget-input13.rw-widget-picker5.rw-multiselect-tag4.rw-btn4.scpay-search__input4.rw-popup-container3.rw-dropdown-list-input2.rw-dropdown-list2.nutrition-select-container .SearchInput input2.rw-multiselect > .rw-widget-picker ul2.el-select__search-container2
SCSS variables in these styles
$scpay-control-heightnot a color: spacing, size or layout$border-color→--border$scpay-control-bordernot a color: spacing, size or layout