Patterns
Searchable Select
When to use:
- the option list is long
- users need text filtering before making a choice
- keeping the field compact still matters
Composition notes:
- render a text input inside
SelectListbox - disable built-in typeahead with
typeahead={false} - filter the rendered
SelectOptionlist from external state - pass the selected label to
SelectValueas children: a filtered-out option is not rendered, so there is nothing to copy - handle Enter in the search field yourself (here it picks the first match), because arrow keys and Enter on the list only work while the trigger has focus
Trade-offs:
- improves discoverability for long lists
- increases keyboard complexity compared with a plain select
import {
Label,
Select,
SelectCombobox,
SelectHiddenSelect,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
Text,
TextField,
TextFieldInput,Clearable Controlled Select
When to use:
- the selected value can be reset
- the field must sync with URL state, filters, or external form logic
Composition notes:
- store selection in React state
- pass
valueandonChangeValue - provide a separate clear action that sets
null, notundefined
Trade-offs:
- keeps state explicit and predictable
- adds parent-state complexity compared with uncontrolled usage
import {
Button,
Label,
Select,
SelectCombobox,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
Text,
} from '@vega-ui/react';
import { useId, useState } from 'react';Select Inside Overlay
When to use:
- a modal, drawer, or sheet contains a single-choice field
- the listbox must coexist with another overlay system
Composition notes:
- render
Selectnormally inside the overlay content - no portal is needed: the listbox renders in the top layer above the overlay
- retest focus restoration and dismissal in the real overlay
Trade-offs:
- preserves normal field behavior inside overlays
- requires more integration testing than isolated component usage
import type { CSSProperties, FormEvent } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Label,
Select,Inline Sentence Select
When to use:
- the control belongs inside readable sentence-like copy
- the choice is lightweight and should feel embedded
Composition notes:
- use
variant='inline' - surround the field with meaningful text
- avoid using inline variant without context
- give the trigger its own
aria-label: the sentence around it is not a label
Trade-offs:
- feels lightweight and embedded
- depends on surrounding copy for clarity and labeling
import {
Select,
SelectCombobox,
SelectHiddenSelect,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
Text,
} from '@vega-ui/react';
export default function InlineVariantInsideSentenceLikeUi() {Rich Option Content
When to use:
- options need iconography or secondary copy
- visual scanning matters more than minimal row density
Composition notes:
- keep the
valuescalar - make the whole option row the selectable unit
- do not split the option into multiple interactive controls
- pass
labelso typeahead matches the name, and a shortSelectValuechild so the trigger does not copy the whole row - options have a fixed height, so multi-line content needs
height: 'auto'on the option
Trade-offs:
- improves scanability for complex choices
- increases row height and semantic complexity
import {
Label,
Select,
SelectCombobox,
SelectHiddenSelect,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
Text,
} from '@vega-ui/react';
import { useId, useState } from 'react';Native Form Select
When to use:
- the selected value should be submitted through normal form mechanisms
- the integration relies on
FormData
Composition notes:
- include
SelectHiddenSelect - keep the select inside the form boundary
Trade-offs:
- improves native form interoperability
- adds one more required part to the composition
import {
Button,
Label,
Select,
SelectCombobox,
SelectHiddenSelect,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
Text,
} from '@vega-ui/react';Notes:
- without
SelectHiddenSelect, the control may still render correctly but the native form contract is weaker
Custom Icon
When to use:
- the trigger icon should hint at the kind of value, not only at the list
- the default chevron does not fit the surrounding UI
Composition notes:
- pass your icon as children of
SelectIcon; they replace the default chevron - keep
SelectIconinsideSelectCombobox, afterSelectValue
Trade-offs:
- adds meaning to the trigger at a glance
- a non-chevron icon makes it less obvious that the field opens a list
import {
Select,
SelectCombobox,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
} from '@vega-ui/react';
import { GlobeIcon } from '@vega-ui/icons';
export default function CustomIcon() {
return (Comparison
Quick Decision Rule
Choose Select if all of the following are true:
- the interaction is single-choice
- the value is
stringornumber - the option set is better hidden than always visible
- native form participation or field semantics matter
Choose another control if visibility, binary state, or richer action content is more important than compact field behavior.
Select vs SegmentedControl
Use Select when:
- there are many options
- space is constrained
- options do not need to stay visible all the time
Use SegmentedControl when:
- there are only a few options
- users benefit from seeing all choices at once
- switching between options should feel immediate
Trade-off:
Selectsaves space but hides optionsSegmentedControlimproves discoverability but consumes layout width
Select vs Radio
Use Select when:
- the option list is long
- the field belongs to a dense form
- the control should stay compact
Use Radio when:
- the option set is short
- users should compare all available choices directly
- explicit single-choice visibility is more important than compactness
Trade-off:
Selectis denserRadiois clearer for comparison
Select vs Checkbox
Use Select when:
- users must choose exactly one option
- the value should behave like a scalar field in a form
Use Checkbox when:
- the interaction is binary
- each control represents an independent yes or no choice
Do not replace multi-checkbox groups with a single Select unless the product model is truly single-choice.
Select vs Switch
Use Select when:
- users are choosing from several values
- the selected value should persist as one field
Use Switch when:
- the interaction is immediate on or off state
- the setting changes directly rather than choosing from a list
Trade-off:
Selectis for enumerated choiceSwitchis for binary state
Select vs Popover
Use Select when:
- the main job is choosing a value
- keyboard option navigation and value semantics are required
Use Popover when:
- the content is not primarily a single-choice field
- the overlay contains actions, help text, or custom UI rather than listbox semantics
Choose This Component When
- the interaction is single-choice
- the value should behave like one compact field
- the option set is long enough that always-visible controls would add noise
- form participation or dense layout matters
Do Not Choose This Component When
- the user benefits from seeing all options at once
- the interaction is binary rather than enumerated
- the overlay content is not actually a field
- the value model depends on object payloads instead of scalar keys
Troubleshooting
Selected Value Is Not Submitted With The Form
Symptom
The control renders and updates visually, but the selected value is missing from native form submission.
Likely Cause
SelectHiddenSelect is missing from the composition.
How To Verify
- submit the surrounding form
- inspect the resulting
FormData - confirm that the expected field name or value is absent
Fix
Render SelectHiddenSelect with a name inside the root Select.
import {
Button,
Label,
Select,
SelectCombobox,
SelectHiddenSelect,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
Text,
} from '@vega-ui/react';Keyboard Users Cannot Pick A Filtered Option
Symptom
A search field inside SelectListbox filters the options, but after typing there is no way to choose one without a mouse.
Likely Cause
Arrow keys and Enter move through the list only while the trigger has focus. Once the search field takes the focus, the select does not handle its keys, and Tab moves the focus out of the list, which closes it.
How To Verify
- open the select and type into the search field
- press arrow keys and Enter
- check whether any option gets selected
Fix
Handle Enter in the search field yourself: pick the first match, close the list and return focus to the trigger. Pass the selected label to SelectValue as children, because a filtered-out option is not rendered.
import {
Label,
Select,
SelectCombobox,
SelectHiddenSelect,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
Text,
TextField,
TextFieldInput,The Select Works Visually But Keyboard Behavior Feels Broken In A Dialog
Symptom
The select opens, but focus movement, close behavior, or keyboard navigation feels inconsistent inside an overlay.
Likely Cause
The composed overlay behavior was validated in isolation but not retested inside Dialog, Drawer, or Sheet.
How To Verify
- render the exact integration inside the real overlay
- test open, close, focus return, and option navigation from the keyboard
Fix
- keep the standard
Selectcomposition intact - retest with the real overlay stack
- avoid custom trigger or option structures that interfere with focus flow
The Placeholder Never Appears
Symptom
placeholder is configured, but the trigger always shows some other content.
Likely Cause
An initial value is already selected, or custom SelectValue children always render content.
How To Verify
- remove
valueanddefaultValue - remove custom children from
SelectValue - check whether the placeholder becomes visible
Fix
- use
placeholderonly when no selected value is present - ensure custom value rendering respects the empty state
Clearing The Value Does Not Update The UI
Symptom
The parent clears the value logically, but the UI still shows the old selection.
Likely Cause
The parent sets the value to undefined. In Select, undefined means uncontrolled, so the component falls back to its own internal state, which still holds an old selection; null is the controlled empty value.
How To Verify
- inspect the current
valueprop - confirm whether
onChangeValueupdates the same source of truth
Fix
Clear the controlled value with null and convert the undefined that onChangeValue can report: onChangeValue={(next) => setValue(next ?? null)}.
import {
Button,
Label,
Select,
SelectCombobox,
SelectIcon,
SelectListbox,
SelectOption,
SelectValue,
Text,
} from '@vega-ui/react';
import { useId, useState } from 'react';Options Render Correctly But Selection Does Not Change
Symptom
Options are visible and clickable, but the selected value does not update as expected.
Likely Cause
SelectOption values do not match the expected scalar value model, or custom option composition broke the selectable row.
How To Verify
- confirm each
SelectOptionhas astringornumbervalue - check whether the whole option row remains the selectable unit
Fix
- keep option values scalar
- avoid splitting one option into multiple competing interactive elements
Read-Only And Disabled Behave Differently Than Expected
Symptom
The control state looks right visually, but focusability or interaction does not match product expectations.
Likely Cause
readOnly and disabled were treated as visual variants rather than behavior variants.
How To Verify
- test focusability and interaction in both states
- confirm whether the control should still receive focus
Fix
- use
disabledwhen interaction must stop entirely - use
readOnlywhen the field should remain focusable but immutable
Object Values Cause Type Or Behavior Problems
Symptom
Type errors, unstable selection, or mismatched values appear when passing full objects as option values.
Likely Cause
The public API is built for string and number values, not object payloads.
How To Verify
- inspect the
valuepassed toSelect - confirm whether
SelectOption value={...}is scalar
Fix
Store a scalar key in Select and resolve the full object in the parent layer.
The Select Looks Correct But The Composition Still Feels Fragile
Symptom
The component appears to work, but small customization changes keep breaking behavior.
Likely Cause
Too much behavior was pushed into custom wrappers instead of the shipped parts.
How To Verify
- compare the current composition to the standard layout in
index.mdandanatomy.md - remove custom wrappers incrementally
Fix
- return to the standard composition first
- reintroduce custom wrappers only where the built-in parts are insufficient
- use
useSelectContextonly for advanced cases