Zetta v1.6

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

Build with the skill

No package to install — hand this to your AI to generate or migrate the component in your stack.

Prompt for your AI
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 placeholder
  • combobox-content — floating panel, maxHeight: 240px, scrolls when options exceed this height
  • combobox-search — optional inline search input, pinned above the scrollable list with a {colors.hairline} divider
  • combobox-item — individual option row
  • combobox-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

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 check icon prefix on the selected item inside the panel.
  • Show combobox-empty when 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-activedescendant on 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-describedby in error state.
  • Contrast: {colors.ink} on {colors.surface-card} is 19.8:1 (AAA). {colors.primary} selected text on transparent is 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}