Combobox
A combobox is a select with a search field inside the panel, so the reader can filter by typing instead of scrolling.
Both open in the same dropdown and store a value the same way.
Figma embed missing — the page this was adopted from shows a Figma frame here, of the dropdown open with a query typed into it. To be rebuilt by hand. There is no generated contact sheet to stand in for it either: no published Figma component set joins the name
Combobox.
Choosing between a select and a combobox
Choose on one question: does the panel need a search field? The panel is the dropdown that opens when the reader clicks the trigger, and closes again when they pick a value or click away. Nothing else separates the two components. Single or multiple, grouped or flat, with icons or without — those are options on either, not reasons to pick one.
A search field is needed when the list holds a lot of items, or when its length is unknown at design time: apps, members, namespaces, tags. The test is whether the reader knows what they are looking for before the panel opens. Someone picking a severity is choosing from a menu. Someone picking a teammate already has a name in mind.
Count the list a customer would see, not the one in your demo environment. A seeded account has three apps and a handful of members. A real one can have hundreds, and the component is chosen for that.
Options
Everything below composes. Build the combination the screen needs rather than picking from a fixed set of variants.
| Option | Use it when | Specimen |
|---|---|---|
| Multiple | The reader stores several values in one field | Multiple and Multiple With Selections |
| Group headers | The options fall into named sets the reader recognises — Pinned and Apps, Assigned and Member | Figma only |
| Leading icon | The icon identifies the option faster than its label, as an avatar does for a person | Figma only |
| Trailing badge | Labels alone are ambiguous. Three apps named House of stroops are only tellable apart by a badge | Figma only |
| Helper text or a message | The trigger's label does not say what the panel filters | Figma only |
"Figma only" means the design library draws it and no Storybook story renders it. That is a gap in the specimen rather than proof the component cannot do it; ui/Select has a With Groups story and ui/Combobox does not, though both are drawn grouped. Check the component's props before assuming either way, and see gaps for what a missing specimen costs.
Single and multiple
Single and multiple are one component. ui/Combobox ships Multiple and Multiple With Selections rather than separate modules.
Selection controls sit on the left, and the single form marks its chosen row with a trailing checkmark. Both follow select.
Keep chosen values visible while a query is typed. A multiple combobox filters the list down as the reader types, which hides what they already picked.
⚠️ The product has two multi-select comboboxes.
ui/Comboboxhas the stories above and one product use.MultiSelectComboboxis a second implementation underLogMetricFormOverlay, with three uses and no story. The library component is the one with a module, a story and this page. Which behaviour is correct is for whoever reconciles them, but the product should not carry both.
Group headers
Use group headers when the options fall into sets the reader already recognises, and leave the list flat when they do not. A header labels a run of options and is not selectable.
Keep header capitalisation consistent across the product. The design library currently mixes PINNED and APPS with Member and Assigned in the same family of menus, which is drift rather than two kinds of header.
Filtering applies to options, not to headers. A group whose options all fall out of the result set goes with them, rather than leaving an empty heading behind.
Badges on rows
Add a trailing badge, such as production or staging, when the label alone is ambiguous.
Do not badge every row. A badge on every row is a column, and it distinguishes nothing.
Helper text and messages
Two different things can sit below the search field, and they are not interchangeable.
- Helper text is one line without a box. Use it to say what the panel filters: "Filter by who an incident is assigned to, or pick a specific member." This is the default, and only when the trigger's label is not enough.
- A message is a bordered box. Reserve it for something the reader has to act on or account for: a truncated result set, a permission limit, a reason the list is empty.
A message carries more weight than an explanation needs, and spending it on ordinary text leaves nothing for a real problem.
Anatomy
| Part | Description |
|---|---|
| Trigger | Button showing the current value plus a chevron |
| Search field | Text field inside the dropdown, for filtering |
| Dropdown panel | Popover container for the filtered results |
| Option row | Text, an optional leading icon (avatar, star), an optional badge |
| Group header (optional) | Section label — Pinned, Apps, Assigned, Member |
| Empty state | Shown when no result matches the query — see empty state, user action kind |
| Helper text (optional) | A short line below the search field clarifying the dropdown's purpose. Only when the label alone isn't enough |
Figma naming
The working file draws this correctly and names the layers loosely. Before these frames are published, the layer names need to match the system's:
| Figma layer | Should be | Why |
|---|---|---|
Pill | Badge | Badge is the construct, the contact sheet and the page. Pill joins nothing |
Search | the text field component | The system has no Search component; the published sheet is text-field |
Checkbox (on option rows) | an option row | These rows hold only text. The real checkbox rows are in the multiple select |
Where it exists
ui-combobox--default (open it in Storybook). One of 3 stories. Drag the bottom edge to resize. ⚠️ The frame renders the component at the frame's own width, so in a narrow pane you are seeing a narrow rendering rather than the canonical one.| axis | value |
|---|---|
| React module | yes — shared UI library |
| React tier | library |
| CSS class | none |
| CSS files | none |
| Storybook | yes |
| Figma | not mapped |
| Uses in product | 1 |
| Uses in admin | 0 |
| Seen as | jsx-tag |
| Join provenance | none |
An empty cell above is a measurement, not an omission: every axis is printed whether or not it is populated. What each one costs is on /gaps.md, linked from the axis name wherever this page has a gap.
Members
A member is attached to its parent at module scope, so it is imported through the parent and versioned with it. Members have no page of their own: they declare no CSS class, carry no story and join no Figma set, and their uses are counted against the parent rather than separately.
None. Nothing is attached to this component as a member.