Components · Forms & selection
Combobox
Searchable single-select dropdown combining a text input trigger with a filterable option list. Distinct from Select (no search) and Multi Select (multiple values). Extracted from zetta-form-v1_3.md (removed in v1.5) as a standalone file in v1.4.
Preview
- Singapore
- Malaysia
- Indonesia
- Thailand
- Vietnam
- Philippines
- Japan
- South Korea
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Combobox" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `combobox` spec and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `combobox` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
┌────────────────────────────────┐
│ Select country… [▼] │ ← combobox-trigger (32px)
└────────────────────────────────┘
↓ open
┌────────────────────────────────┐
│ [🔍 Search… ] │ ← combobox-search (36px, pinned top)
│ ───────────────────────────── │ ← hairline separator
│ ✓ Indonesia (primary) │ ← combobox-item-selected
│ France │ ← combobox-item
│ Germany │
│ (No results) │ ← combobox-empty
└────────────────────────────────┘ ← combobox-content (maxHeight 240px, scrolls)
Parts:
combobox-trigger— button-like trigger showing the selected value or placeholdercombobox-content— floating panel,maxHeight: 240px, scrolls when options exceed this heightcombobox-search— optional inline search input, pinned above the scrollable list with a{colors.hairline}dividercombobox-item— individual option rowcombobox-empty— shown when search yields no results
Trigger
| State | Border | Notes |
|---|---|---|
| Default (empty) | {colors.border-strong} |
Placeholder shown |
| Default (value selected) | {colors.border-strong} |
Selected label shown |
| Hover | {colors.border-strong} + {elevation.shadow.raised} |
— |
| Focus | {colors.border-focus} + 2px ring |
— |
| Open | {colors.primary} + primary-bg ring |
Panel visible |
| Invalid | {colors.danger} |
Validation error |
| Disabled | {colors.disabled-border} |
not-allowed, no panel |
Item
| State | Background | Text |
|---|---|---|
| Default | transparent |
{colors.ink} |
| Hover | {colors.accent} |
{colors.ink} |
| Focused (keyboard) | {colors.accent} + focus outline |
{colors.ink} |
| Selected | transparent |
{colors.primary} / 500 + check icon |
| Disabled | transparent |
{colors.disabled-text} |
When to use
- Selecting one value from a long list (20+ options) where searching is necessary.
- Country, timezone, currency, language, or user selection fields.
When not to use
- Short lists (under 10 options) — use Select.
- Multiple selections — use Multi Select.
- Action triggers — use Dropdown.
Do
- Default filter by label prefix or substring — match from any position in the string.
- Show the current selected value in the trigger when the panel is closed.
- Show a
checkicon prefix on the selected item inside the panel. - Show
combobox-emptywhen search yields zero results — never show an empty panel.
Don't
- Don't close the panel on every keystroke — keep it open while the user is typing.
- Don't use combobox for free-text entry — the user must select from existing options.
- Trigger placeholder: "Select [noun]…" — "Select country…", "Select language…".
- Search placeholder: "Search…" or "Search [noun]…" — "Search countries…".
- Selected item display: the option's label, not its value key.
- Empty state: "No results for '[query]'." — include the actual query.
- Option labels: consistent format — all title case or all sentence case, never mixed.
Keyboard
| Key | Behavior |
|---|---|
Tab |
Focus the trigger |
Enter / Space |
Open the panel |
Down Arrow |
Open panel; move focus to first item |
Up / Down Arrow |
Navigate between items in the panel |
Type characters |
Filter the list (search input focuses automatically) |
Enter |
Select the focused item, close panel |
Escape |
Close panel without selecting, return focus to trigger |
Tab (panel open) |
Close panel, move focus to next element |
Accessibility
- Trigger ARIA:
role="combobox",aria-haspopup="listbox",aria-expanded="true/false",aria-controls="listbox-id". - Panel ARIA:
role="listbox",id="listbox-id". - Item ARIA:
role="option",aria-selected="true/false",aria-disabled="true"for disabled items. aria-activedescendanton the trigger pointing to the currently focused item id.- Search input inside the panel:
aria-label="Search options", not independently labelled as a combobox. aria-invalid="true"+aria-describedbyin error state.- Contrast:
{colors.ink}on{colors.surface-card}is 19.8:1 (AAA).{colors.primary}selected text ontransparentis 6.06:1 (AA).
Spec
Values and token references resolved straight from the Zetta spec.
combobox-trigger
- backgroundColor
- {colors.surface-card}
- textColor
- {colors.ink}
- placeholderColor
- {colors.placeholder}
- placeholderStyle
- italic
- borderColor
- {colors.border-strong}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 400
- lineHeight
- 1.6
- height
- 32px
- paddingX
- 14px
- chevronSize
- 16px
- chevronColor
- {colors.muted}
- display
- flex
- alignItems
- center
- justifyContent
- space-between
- cursor
- pointer
combobox-trigger-hover
- borderColor
- {colors.border-strong}
- boxShadow
- {elevation.shadow.raised}
combobox-trigger-focus
- borderColor
- {colors.border-focus}
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
combobox-trigger-open
- borderColor
- {colors.primary}
- outline
- 2px solid {colors.primary-bg}
- outlineOffset
- 2px
combobox-trigger-invalid
- borderColor
- {colors.danger}
combobox-trigger-invalid-focus
- borderColor
- {colors.danger}
- outline
- 2px solid {colors.danger}
- outlineOffset
- 2px
combobox-trigger-disabled
- backgroundColor
- {colors.disabled-bg}
- textColor
- {colors.disabled-text}
- borderColor
- {colors.disabled-border}
- cursor
- not-allowed
combobox-content
- backgroundColor
- {colors.popover}
- textColor
- {colors.popover-foreground}
- borderColor
- {colors.hairline}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- padding
- 6px
- shadow
- {elevation.shadow.overlay}
- maxHeight
- 240px
- overflowY
- auto
combobox-search
- height
- 36px
- paddingX
- 12px
- paddingY
- {spacing.sm}
- fontFamily
- Geist
- fontSize
- 14px
- textColor
- {colors.ink}
- placeholderColor
- {colors.placeholder}
- borderBottomColor
- {colors.hairline}
- borderBottomWidth
- 1px
combobox-item
- textColor
- {colors.ink}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 400
- borderRadius
- {rounded.base}
- paddingX
- 12px
- paddingY
- 10px
- cursor
- pointer
- display
- flex
- alignItems
- center
- gap
- 8px
combobox-item-hover
- backgroundColor
- {colors.accent}
combobox-item-focused
- backgroundColor
- {colors.accent}
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- -2px
combobox-item-selected
- textColor
- {colors.primary}
- fontWeight
- 500
- checkIconColor
- {colors.primary}
- checkIconSize
- {icons.sizeMap.md}
combobox-item-disabled
- textColor
- {colors.disabled-text}
- cursor
- not-allowed
combobox-empty
- textColor
- {colors.muted}
- fontFamily
- Geist
- fontSize
- 14px
- textAlign
- center
- paddingX
- 12px
- paddingY
- {spacing.md}