MiHCM
primitives/@mihcm/ui·v0.21.0·stable

Select

Custom dropdown for single-value selection. Composable: Select, SelectTrigger, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator.

Select

Select supports two APIs:

  • Select + sub-components: a lightweight, composable single-value listbox for simple forms and native parity.
  • ReactSelect, ReactAsyncSelect, ReactCreatableSelect, and ReactAsyncCreatableSelect: web-only wrappers around react-select for searchable, async, multi-value, creatable, grouped, animated, and highly customized selection.

Use cases

  • Form fields where the user picks exactly one value from a moderate-length list.
  • Settings with mutually exclusive options that don't all need to be visible.
  • Filters and sorting controls in toolbars.
  • People, workflow, and tag selectors that need search, multi-select, remote loading, or new-value creation.

When NOT to use

  • For short lists (2-5 options) where all should be visible — use RadioGroup.
  • For persistent filter panels with many visible boolean values — use CheckboxGrid.
  • For command search and app navigation — use Combobox or Command.

Import

import {
  Select,
  SelectTrigger,
  SelectContent,
  SelectItem,
  SelectGroup,
  SelectLabel,
  SelectSeparator,
} from '@mihcm/ui/Select';
import {
  ReactSelect,
  ReactAsyncSelect,
  ReactCreatableSelect,
  ReactAsyncCreatableSelect,
  getReactSelectAnimatedComponents,
  reactSelectAnimatedComponents,
  type DefaultReactSelectOption,
} from '@mihcm/ui/Select';

Basic usage

const [value, setValue] = useState('apple');
 
<Select value={value} onValueChange={setValue}>
  <SelectTrigger placeholder="Pick a fruit" />
  <SelectContent>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectItem value="banana">Banana</SelectItem>
    <SelectItem value="cherry">Cherry</SelectItem>
  </SelectContent>
</Select>

With groups

<Select value={value} onValueChange={setValue}>
  <SelectTrigger placeholder="Choose a role" />
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Engineering</SelectLabel>
      <SelectItem value="fe">Frontend</SelectItem>
      <SelectItem value="be">Backend</SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectGroup>
      <SelectLabel>Design</SelectLabel>
      <SelectItem value="ux">UX Designer</SelectItem>
      <SelectItem value="ui">UI Designer</SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>

Composition

  • Use SelectGroup with SelectLabel to organize options into named sections.
  • SelectSeparator adds a visual divider between groups.
  • disabled on SelectItem prevents selection without removing the option from the list.
  • Use the React Select-powered exports when a select needs search, async data, multiple values, creatable values, rich labels, or menu portals.

React Select capability guide

The React Select-powered wrappers keep React Select's prop surface and add MiHCM defaults for unstyled, classNamePrefix, and token-based classNames.

CapabilityAPIUse it for
Searchable selectReactSelect isSearchablePeople, country, office, and workflow pickers.
Multi-selectReactSelect isMultiOwners, tags, segments, permissions.
Async loadingReactAsyncSelect loadOptionsServer-backed option lists, large directories.
Creatable valuesReactCreatableSelectTags, draft workflow names, controlled vocabularies.
Async + creatableReactAsyncCreatableSelectRemote tag search with allowed local creation.
Grouped optionsoptions={[{ label, options }]}Separating teams, locations, statuses, or categories.
Rich option labelsformatOptionLabel or componentsMetadata, avatars, status badges, secondary descriptions.
Controlled statevalue, inputValue, menuIsOpenForm libraries, guided flows, validation-heavy screens.
Styling overridesclassNames, className, classNamePrefix, stylesTailwind token overrides without hardcoded colors.
Portal renderingmenuPortalTargetDialogs, sheets, tables, and clipped containers.
Animationcomponents={reactSelectAnimatedComponents}Multi-value chip transitions.

Use a popup select when selection is a filter or command attached to a toolbar trigger, not a form field. The trigger should summarize the current selection, and the popup should let people filter through options without leaving the current surface.

<ReactSelect<OwnerOption, true>
  aria-label="Filter by owner"
  isMulti
  closeMenuOnSelect={false}
  hideSelectedOptions={false}
  controlShouldRenderValue={false}
  value={selectedOwners}
  onChange={setSelectedOwners}
  options={ownerOptions}
  formatOptionLabel={OwnerOptionLabel}
  placeholder={selectedOwners.length > 0 ? `Owners: ${selectedOwners.length} selected` : 'Owner'}
  classNames={{
    control: ({ isFocused }) =>
      cn(
        'min-h-10 w-fit min-w-48 cursor-pointer rounded-md border bg-background px-2.5 text-body-sm shadow-sm',
        'hover:bg-muted',
        isFocused ? 'border-primary ring-2 ring-ring/20' : 'border-border',
      ),
    placeholder: () => 'font-medium text-foreground',
  }}
/>

Popup select rules:

  • Use it in toolbars, table filters, issue queues, and quick filter panels.
  • Keep trigger copy short: Owner, Owner: Yashi, or Owners: 3 selected.
  • Use controlShouldRenderValue={false} when the trigger should stay compact.
  • Use closeMenuOnSelect={false} for multi-select filters.
  • Use hideSelectedOptions={false} when selected options need to remain visible and toggleable.
  • Do not use popup select for required form fields; use the normal field-style select.

Web-only advanced usage

interface OwnerOption extends DefaultReactSelectOption {
  meta: string;
}
 
<ReactSelect<OwnerOption, true>
  isMulti
  closeMenuOnSelect={false}
  components={getReactSelectAnimatedComponents<OwnerOption, true>()}
  options={ownerOptions}
  classNames={{
    control: ({ isFocused }) =>
      cn('min-h-11 rounded-md border bg-background', isFocused && 'ring-2 ring-ring/20'),
    multiValue: () => 'rounded-md bg-primary/10 text-primary',
  }}
/>
<ReactAsyncCreatableSelect<OwnerOption, true>
  isMulti
  cacheOptions
  defaultOptions
  loadOptions={(inputValue) =>
    fetch(`/api/owners?q=${encodeURIComponent(inputValue)}`).then((response) => response.json())
  }
  placeholder="Search or create owner..."
/>

Security and accessibility

  • The composable Select is controlled-first. When a user selects an item, the visible label updates from that selected item immediately instead of waiting for a later render cycle.
  • Validate remote loadOptions responses with a schema before setting them as options.
  • Debounce or cancel remote requests in product code when the endpoint is expensive.
  • Do not put secrets, internal IDs, access tokens, or raw model output in labels.
  • Do not render raw HTML inside custom React Select components.
  • Keep a visible label with aria-label or aria-labelledby when the surrounding form label is not programmatically connected.
  • Preserve keyboard behavior when replacing internal components through the components prop.
  • Combobox — searchable dropdown for long option lists.
  • RadioGroup — visible options for short lists (2-7 items).
  • DropdownMenu — action list, not a form field.