Zetta v1.6

Components · Forms & selection

Select

A styled native <select> — accessible and keyboard-operable for free — with a custom chevron. The trigger matches the text input; the option list is the platform's.

Preview

  • Alice Johnson
  • Bob Smith
  • Carol Davis
  • Dan Lee

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 "Select" 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 `select` spec (variants: default, focus, invalid, disabled) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `select` 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

╭─────────────────────────────╮
│ Option label            ▾   │  ← select-control
╰─────────────────────────────╯
╭─────────────────────────────╮
│ 🔍 Search...                │  ← select-input (when searchable)
│ ─────────────────────────── │
│  Option A                   │  ← select-option
│  Option B          ✓        │  ← select-option-selected
│  Option C                   │
╰─────────────────────────────╯
State Trigger border Fill Notes
Default (empty) {colors.border-strong} {colors.surface-card} Placeholder shown
Default (has value) {colors.border-strong} {colors.surface-card} Selected value shown
Hover {colors.muted} {colors.surface-card}
Focus / Open {colors.primary} + glow {colors.surface-card} Dropdown panel appears
Disabled {colors.disabled-border} {colors.disabled-bg} not-allowed
Error {colors.danger} + glow {colors.surface-card} Validation message
Read-only {colors.hairline} {colors.surface-muted} No dropdown opens

When to use

  • Standard: short static option lists (< 20 items).
  • Async: long or server-side option sets, user search is needed.
  • Creatable / Async Creatable: user needs to define new values on the fly (tags, labels, assignees).
  • Popup: compact toolbar filters, column visibility toggles.
  • Checkbox Select: multi-select where users compare options — checkboxes make selection state explicit.
  • Radio Select: constrained single choice where the radio pattern communicates mutual exclusivity.

When not to use

Do

  • Always provide a placeholder with a meaningful prompt ("Select assignee").
  • Show group headings for option lists with 3+ categories.
  • For async selects, display a loading indicator while options fetch.

Don't

  • Don't use Creatable when the option set is closed — it confuses users about whether free text is valid.
  • Placeholder: action-oriented prompt, 2–4 words. "Select assignee", "Choose a project", "Filter by status". Never "Select…" alone.
  • Option labels: concise noun phrases. Sentence case. Avoid special characters except where semantically required (e.g. status codes).
  • Group headings: short uppercase nouns — "DEPARTMENTS", "ROLES". Keep under 20 characters.
  • Creatable placeholder: "Create option" or "Add [noun]" — make it clear the user is creating, not selecting.
  • No options message: "No options" (default) or context-specific: "No assignees found", "No results for 'abc'".

Keyboard

Key Behavior
Tab Move focus to the select trigger
Enter / Space Open the dropdown
Up / Down Arrow Navigate options
Home / End Jump to first / last option
Type characters Jump to option matching typed prefix
Enter Select focused option, close dropdown
Escape Close without selecting
Backspace Remove last multi-value chip

Accessibility

  • ARIA: role="combobox" on the control; aria-expanded toggles on open/close; aria-haspopup="listbox".
  • Menu: role="listbox"; each option role="option" with aria-selected.
  • Multi-value chips: each chip should have aria-label="Remove [value]" on its remove button.
  • Async loading: announce loading state with aria-live="polite" region.

Spec

Values and token references resolved straight from the Zetta spec.

select-container
position
relative
width
100%
select-control
display
flex
alignItems
center
justifyContent
space-between
paddingX
{spacing.sm}
paddingY
{spacing.xs}
minHeight
36px
backgroundColor
{colors.surface-card}
borderColor
{colors.hairline}
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
400
lineHeight
1.40
textColor
{colors.ink}
cursor
pointer
gap
{spacing.xs}
select-control-hover
borderColor
{colors.border-strong}
select-control-focus
borderColor
{colors.border-focus}
outline
2px solid {colors.border-focus}
outlineOffset
-1px
select-control-disabled
backgroundColor
{colors.surface-muted}
borderColor
{colors.hairline}
textColor
{colors.disabled-text}
cursor
not-allowed
select-control-error
borderColor
{colors.danger}
select-placeholder
textColor
{colors.muted}
select-value
textColor
{colors.ink}
overflow
hidden
textOverflow
ellipsis
whiteSpace
nowrap
select-multi-value
backgroundColor
{colors.primary-bg}
borderRadius
{rounded.sm}
paddingX
{spacing.xs}
paddingY
2px
fontSize
12px
textColor
{colors.primary}
display
inline-flex
alignItems
center
gap
{spacing.xs}
margin
2px
select-multi-value-remove
iconSize
{icons.sizeMap.sm}
iconColor
{colors.primary}
cursor
pointer
select-indicator-separator
width
1px
backgroundColor
{colors.hairline}
alignSelf
stretch
marginY
4px
select-dropdown-indicator
iconSize
{icons.sizeMap.md}
iconColor
{colors.muted}
padding
{spacing.xs}
select-clear-indicator
iconSize
{icons.sizeMap.md}
iconColor
{colors.muted}
cursor
pointer
padding
{spacing.xs}
select-menu
backgroundColor
{colors.popover}
borderColor
{colors.hairline}
borderWidth
1px
borderRadius
{rounded.base}
shadow
{elevation.shadow.overlay}
marginTop
4px
zIndex
{elevation.z-index.popup}
overflow
hidden
select-menu-list
maxHeight
300px
overflowY
auto
padding
{spacing.xs}
select-option
paddingX
{spacing.sm}
paddingY
{spacing.xs}
borderRadius
{rounded.sm}
fontSize
14px
fontFamily
Geist
textColor
{colors.ink}
cursor
pointer
display
flex
alignItems
center
gap
{spacing.sm}
minHeight
32px
select-option-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
select-option-selected
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
fontWeight
500
select-option-focused
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
outline
2px solid {colors.border-focus}
outlineOffset
-2px
select-option-disabled
textColor
{colors.disabled-text}
cursor
not-allowed
select-group-heading
fontSize
11px
fontWeight
500
textColor
{colors.muted}
textTransform
uppercase
letterSpacing
0.06em
paddingX
{spacing.sm}
paddingY
{spacing.xs}
select-input
fontFamily
Geist
fontSize
14px
border
none
outline
none
backgroundColor
transparent
textColor
{colors.ink}
flexGrow
1
minWidth
80px
select-no-options
paddingX
{spacing.sm}
paddingY
{spacing.md}
textColor
{colors.muted}
fontSize
14px
textAlign
center
select-loading
paddingX
{spacing.sm}
paddingY
{spacing.md}
textColor
{colors.muted}
fontSize
14px
select-create-option
fontStyle
italic
textColor
{colors.primary}
select-popup-trigger
display
inline-flex
alignItems
center
gap
{spacing.xs}
paddingX
{spacing.sm}
paddingY
{spacing.xs}
borderRadius
{rounded.base}
fontSize
14px
fontFamily
Geist
textColor
{colors.ink}
borderColor
{colors.hairline}
borderWidth
1px
backgroundColor
{colors.surface-card}
cursor
pointer
select-popup-trigger-active
borderColor
{colors.border-focus}
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
select-checkbox-option
display
flex
alignItems
center
gap
{spacing.sm}
select-radio-option
display
flex
alignItems
center
gap
{spacing.sm}

Known gaps

  • Combobox fixed-height variant (fixed menu height + internal overflow-y scroll) — not yet specified.
  • Popup select placement fallback — flip behavior for constrained viewports not yet specified.
  • Group keyboard navigation — arrow key behavior across group boundaries not yet documented.