frontend/src/components/ui/Rocket/Combobox/Combobox.spec.md
Combobox is a searchable dropdown for choosing one value from a filtered list. The trigger is an input field that filters options as the user types. Reuses the same visual tokens as Input/Select (bg, border, focus ring, sizing).
Wraps shadcn Combobox (@base-ui/react Combobox primitive).
v1 scope: Single-select only. Multi-select (chips) deferred to v2.
| Component | Wraps | Token overrides |
|---|---|---|
ComboboxInput | shadcn ComboboxInput | Same tokens as Select trigger — bg, border, focus, hover, disabled, error, sizes |
ComboboxContent | shadcn ComboboxContent | bg-surface-layer-01, border-weak, rounded-lg (10px), elevation-300 |
ComboboxItem | shadcn ComboboxItem | 32px height, text-default, hover → interactive-hover, rounded-md |
ComboboxEmpty | plain div (peer-based) | text-placeholder, centered, hides when list has items |
ComboboxList | shadcn ComboboxList | Adds tw-peer for empty state detection |
ComboboxTrigger | shadcn ComboboxTrigger | Icon colour icon-default |
ComboboxLabel | shadcn ComboboxLabel | text-placeholder, text-xs, font-semibold |
ComboboxSeparator | shadcn ComboboxSeparator | bg-border-weak |
Combobox (root) | shadcn Combobox | Provides ComboboxAnchorContext for positioning |
ComboboxValue, ComboboxGroup, ComboboxCollection
| Prop | Type | Values | Default |
|---|---|---|---|
| size | string | large | default | small | default |
| className | string | — | — |
| disabled | boolean | — | false |
| readOnly | boolean | — | false |
| loading | boolean | — | false |
| showTrigger | boolean | — | true |
| showClear | boolean | — | false |
| Value | Height | Font size | Tailwind |
|---|---|---|---|
| large | 40px | 14px / 20px | tw-h-10 tw-text-lg |
| default | 32px | 12px / 18px | tw-h-8 tw-text-base |
| small | 28px | 12px / 18px | tw-h-7 tw-text-base |
| Element | State | ToolJet token | Tailwind class |
|---|---|---|---|
| background | default | --background-surface-layer-01 | tw-bg-background-surface-layer-01 |
| border | default | --border-default | tw-border-border-default |
| border | hover | --border-strong | hover:tw-border-border-strong |
| text | default | --text-default | tw-text-text-default |
| placeholder | default | --text-placeholder | placeholder:tw-text-text-placeholder |
| shadow | default | --elevation-000 | tw-shadow-elevation-000 |
| focus ring | focus | --interactive-focus-outline | focus:tw-ring-2 focus:tw-ring-[var(--interactive-focus-outline)] focus:tw-ring-offset-1 |
| border | error | --border-danger-strong | aria-[invalid=true]:tw-border-border-danger-strong |
| background | error | --background-error-weak | aria-[invalid=true]:tw-bg-background-error-weak |
| background | disabled | --background-surface-layer-02 | disabled:tw-bg-background-surface-layer-02 |
| text | disabled | --text-disabled | disabled:tw-text-text-disabled |
| border | disabled | none (no border) | disabled:tw-border-transparent |
| Element | Token | Tailwind class |
|---|---|---|
| background | --background-surface-layer-01 | tw-bg-background-surface-layer-01 |
| border | --border-weak | tw-border-border-weak |
| border radius | 10px | tw-rounded-[10px] |
| shadow | elevation-300 | tw-shadow-elevation-300 |
| padding | 8px | tw-p-2 |
| Element | State | Token | Tailwind class |
|---|---|---|---|
| text | default | --text-default | tw-text-text-default |
| height | — | 32px | tw-h-8 |
| padding | — | — | tw-px-2 tw-py-1.5 |
| border-radius | — | 6px | tw-rounded-md |
| background | hover/focus | --interactive-hover | focus:tw-bg-interactive-hover |
| check icon | selected | --text-brand | tw-text-text-brand |
| Element | Token | Tailwind class |
|---|---|---|
| text | --text-placeholder | tw-text-text-placeholder |
| padding | — | tw-py-6 |
| alignment | center | tw-text-center |
ReactNode) — defaults to Search from lucideshowClear propshowTrigger propReactNode) — via children composition| State | Trigger | Notes |
|---|---|---|
| default | normal appearance | — |
| hover | --border-strong border | — |
| focus | focus ring | — |
| disabled | muted bg, text, no border | pointer-events-none |
| readOnly | normal appearance, no typing | input is not editable, dropdown still works |
| loading | spinner replaces chevron | replaces trigger icon with Spinner |
| error | red border + bg | via aria-invalid="true" |
Shape C — sizes only (no variant axis). States handled via CSS pseudo-classes and aria attributes.
<Combobox>
<ComboboxInput placeholder="Search..." />
<ComboboxContent>
<ComboboxList>
<ComboboxItem value="react">React</ComboboxItem>
<ComboboxItem value="vue">Vue</ComboboxItem>
<ComboboxItem value="svelte">Svelte</ComboboxItem>
</ComboboxList>
<ComboboxEmpty>No results found.</ComboboxEmpty>
</ComboboxContent>
</Combobox>
<Field>
<FieldLabel>Framework</FieldLabel>
<Combobox>
<ComboboxInput placeholder="Search frameworks..." />
<ComboboxContent>
<ComboboxList>
<ComboboxItem value="react">React</ComboboxItem>
<ComboboxItem value="vue">Vue</ComboboxItem>
</ComboboxList>
<ComboboxEmpty>No results found.</ComboboxEmpty>
</ComboboxContent>
</Combobox>
</Field>
<Combobox>
<ComboboxInput placeholder="Select country..." showClear />
<ComboboxContent>
<ComboboxList>
<ComboboxGroup>
<ComboboxLabel>Americas</ComboboxLabel>
<ComboboxItem value="us">United States</ComboboxItem>
<ComboboxItem value="ca">Canada</ComboboxItem>
</ComboboxGroup>
<ComboboxSeparator />
<ComboboxGroup>
<ComboboxLabel>Europe</ComboboxLabel>
<ComboboxItem value="uk">United Kingdom</ComboboxItem>
<ComboboxItem value="de">Germany</ComboboxItem>
</ComboboxGroup>
</ComboboxList>
<ComboboxEmpty>No countries found.</ComboboxEmpty>
</ComboboxContent>
</Combobox>
ChevronDown (lucide) — same as Select.Search (lucide) — shown as leading icon inside input.X (lucide) — shown when showClear is true and a value is selected.Spinner component.ComboboxChips.Combobox itself does not auto-truncate. To reveal long content on hover, opt in by composing with TruncatingText. It uses the browser's native title attribute, so no provider or extra wiring is needed.
ComboboxItem children render as the row label. Wrap them in TruncatingText per row — string children make auto-detection work:
<Combobox items={queries}>
<ComboboxInput placeholder="Search query..." />
<ComboboxContent>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
<TruncatingText>{item.name}</TruncatingText>
</ComboboxItem>
)}
</ComboboxList>
<ComboboxEmpty>No results found.</ComboboxEmpty>
</ComboboxContent>
</Combobox>
The Combobox input trigger handles overflow itself, no opt-in required:
comboboxInputVariants carries [&_input]:tw-truncate, so blurred input values that overflow show … at the right edge. Focused inputs scroll horizontally per browser default.ComboboxInput calls the useInputOverflowTitle hook, which detects overflow on the inner input and sets input.title = input.value when overflowing (removing it when the value fits). On blur, input.scrollLeft is reset to 0 so the ellipsis reappears reliably.Limitation: programmatic writes to input.value (Base UI updating the value after a dropdown pick) do not fire input events per HTML spec. The title attribute may be momentarily stale after a selection change until the next user interaction (typing, blur, resize). Visually the ellipsis is always correct.
Same reasoning as Select. Truncation is a layout decision per callsite. Forcing it into the primitive would require modifying the primitive itself and would couple every consumer to that decision.