Pro--% off in--d : --h : --m : --s
HeroUI

Select

Migration guide for Select from HeroUI v2 to v3

Refer to the v3 Select documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.

Structure Changes

In v2, Select used a simple structure with props:

import { Select, SelectItem } from "@heroui/react";

export default function App() {
  return (
    <Select label="Select animal" placeholder="Choose one">
      <SelectItem key="cat">Cat</SelectItem>
      <SelectItem key="dog">Dog</SelectItem>
    </Select>
  );
}

In v3, Select requires compound components and uses ListBox for items:

import { Select, Label, ListBox } from "@heroui/react";

export default function App() {
  return (
    <Select placeholder="Choose one">
      <Label>Select animal</Label>
      <Select.Trigger>
        <Select.Value />
        <Select.Indicator />
      </Select.Trigger>
      <Select.Popover>
        <ListBox>
          <ListBox.Item id="cat" textValue="Cat">
            Cat
            <ListBox.ItemIndicator />
          </ListBox.Item>
          <ListBox.Item id="dog" textValue="Dog">
            Dog
            <ListBox.ItemIndicator />
          </ListBox.Item>
        </ListBox>
      </Select.Popover>
    </Select>
  );
}

Key Changes

1. Component Structure

v2: Simple Select with SelectItem children
v3: Compound components (Select.Trigger, Select.Value, Select.Indicator, Select.Popover) with ListBox for items

2. Item Components

v2: SelectItem, SelectSection v3: ListBox.Item, ListBox.Section (with Header for section titles and Separator between sections)

3. Prop Changes

v2 Propv3 LocationNotes
selectedKeysvalueChanged from Set/array to single value or array
onSelectionChangeonChangeRenamed event handler
defaultSelectedKeysdefaultValueRenamed prop
labelUse Label component
descriptionUse Description component
errorMessageUse FieldError component
variantvariantSimplified to primary | secondary only
colorRemoved (use Tailwind CSS)
sizeRemoved (use Tailwind CSS)
radiusRemoved (use Tailwind CSS)
classNamesUse className props on individual components
startContentCustomize Select.Trigger directly
endContentCustomize Select.Trigger directly
selectorIconCustomize Select.Indicator children
isClearableImplement clear button manually
renderValueUse Select.Value render prop
labelPlacementLabels are always outside
isRequiredisRequiredStill exists
disabledKeysdisabledKeysStill exists
isOpenisOpenNew: controls the open state of the popover (controlled)
defaultOpendefaultOpenNew: sets the default open state of the popover (uncontrolled)
onOpenChangeonOpenChangeNew: handler called when the open state changes
selectionModeselectionModeStill exists
disableAnimationRemoved (animations handled differently)
popoverProps, listboxProps, scrollShadowPropsUse props on Select.Popover, ListBox, etc. directly

Migration Examples

Selection

import { useState } from "react";

{/* Single selection */}
const [singleValue, setSingleValue] = useState(new Set([]));
<Select
  selectedKeys={singleValue}
  onSelectionChange={setSingleValue}
  label="Select animal"
>
  <SelectItem key="cat">Cat</SelectItem>
  <SelectItem key="dog">Dog</SelectItem>
</Select>

{/* Multiple selection */}
const [multiValue, setMultiValue] = useState(new Set([]));
<Select
  selectionMode="multiple"
  selectedKeys={multiValue}
  onSelectionChange={setMultiValue}
  label="Select animals"
>
  <SelectItem key="cat">Cat</SelectItem>
  <SelectItem key="dog">Dog</SelectItem>
</Select>
import { useState } from "react";
import type { Key } from "@heroui/react";

{/* Single selection */}
const [singleValue, setSingleValue] = useState<Key | null>(null);
<Select value={singleValue} onChange={setSingleValue} placeholder="Choose one">
  <Label>Select animal</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator />
  </Select.Trigger>
  <Select.Popover>
    <ListBox>
      <ListBox.Item id="cat" textValue="Cat">
        Cat
        <ListBox.ItemIndicator />
      </ListBox.Item>
      <ListBox.Item id="dog" textValue="Dog">
        Dog
        <ListBox.ItemIndicator />
      </ListBox.Item>
    </ListBox>
  </Select.Popover>
</Select>

{/* Multiple selection */}
const [multiValue, setMultiValue] = useState<Key[]>([]);
<Select
  selectionMode="multiple"
  value={multiValue}
  onChange={setMultiValue}
  placeholder="Choose multiple"
>
  <Label>Select animals</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator />
  </Select.Trigger>
  <Select.Popover>
    <ListBox>
      <ListBox.Item id="cat" textValue="Cat">
        Cat
        <ListBox.ItemIndicator />
      </ListBox.Item>
      <ListBox.Item id="dog" textValue="Dog">
        Dog
        <ListBox.ItemIndicator />
      </ListBox.Item>
    </ListBox>
  </Select.Popover>
</Select>

Form Validation

{/* With description */}
<Select
  label="Select animal"
  description="Choose your favorite"
  placeholder="Choose one"
>
  <SelectItem key="cat">Cat</SelectItem>
</Select>

{/* With validation */}
<Select
  isInvalid
  errorMessage="Please select an option"
  label="Select animal"
>
  <SelectItem key="cat">Cat</SelectItem>
</Select>
import { Label, Description, FieldError } from "@heroui/react";

{/* With description */}
<Select placeholder="Choose one">
  <Label>Select animal</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator />
  </Select.Trigger>
  <Description>Choose your favorite</Description>
  <Select.Popover>
    <ListBox>
      <ListBox.Item id="cat" textValue="Cat">
        Cat
        <ListBox.ItemIndicator />
      </ListBox.Item>
    </ListBox>
  </Select.Popover>
</Select>

{/* With validation */}
<Select isInvalid placeholder="Choose one">
  <Label>Select animal</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator />
  </Select.Trigger>
  <Select.Popover>
    <ListBox>
      <ListBox.Item id="cat" textValue="Cat">
        Cat
        <ListBox.ItemIndicator />
      </ListBox.Item>
    </ListBox>
  </Select.Popover>
  <FieldError>Please select an option</FieldError>
</Select>

With Sections

import { Select, SelectItem, SelectSection } from "@heroui/react";

<Select label="Select animal">
  <SelectSection title="Mammals">
    <SelectItem key="cat">Cat</SelectItem>
    <SelectItem key="dog">Dog</SelectItem>
  </SelectSection>
  <SelectSection title="Birds">
    <SelectItem key="eagle">Eagle</SelectItem>
    <SelectItem key="parrot">Parrot</SelectItem>
  </SelectSection>
</Select>
import { Select, Label, ListBox, Header, Separator } from "@heroui/react";

<Select placeholder="Choose one">
  <Label>Select animal</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator />
  </Select.Trigger>
  <Select.Popover>
    <ListBox>
      <ListBox.Section>
        <Header>Mammals</Header>
        <ListBox.Item id="cat" textValue="Cat">
          Cat
          <ListBox.ItemIndicator />
        </ListBox.Item>
        <ListBox.Item id="dog" textValue="Dog">
          Dog
          <ListBox.ItemIndicator />
        </ListBox.Item>
      </ListBox.Section>
      <Separator />
      <ListBox.Section>
        <Header>Birds</Header>
        <ListBox.Item id="eagle" textValue="Eagle">
          Eagle
          <ListBox.ItemIndicator />
        </ListBox.Item>
        <ListBox.Item id="parrot" textValue="Parrot">
          Parrot
          <ListBox.ItemIndicator />
        </ListBox.Item>
      </ListBox.Section>
    </ListBox>
  </Select.Popover>
</Select>

Controlled Open State

{/* v2 did not have controlled open state */}
<Select label="Select animal">
  <SelectItem key="cat">Cat</SelectItem>
  <SelectItem key="dog">Dog</SelectItem>
</Select>
import { useState } from "react";
import { Select, Label, ListBox } from "@heroui/react";

const [isOpen, setIsOpen] = useState(false);

<Select isOpen={isOpen} onOpenChange={setIsOpen} placeholder="Choose one">
  <Label>Select animal</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator />
  </Select.Trigger>
  <Select.Popover>
    <ListBox>
      <ListBox.Item id="cat" textValue="Cat">
        Cat
        <ListBox.ItemIndicator />
      </ListBox.Item>
      <ListBox.Item id="dog" textValue="Dog">
        Dog
        <ListBox.ItemIndicator />
      </ListBox.Item>
    </ListBox>
  </Select.Popover>
</Select>

Disabled Options

import { Select, SelectItem } from "@heroui/react";

<Select label="Select animal" disabledKeys={["dog"]}>
  <SelectItem key="cat">Cat</SelectItem>
  <SelectItem key="dog">Dog</SelectItem>
  <SelectItem key="parrot">Parrot</SelectItem>
</Select>
import { Select, Label, ListBox } from "@heroui/react";

<Select disabledKeys={["dog"]} placeholder="Choose one">
  <Label>Select animal</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator />
  </Select.Trigger>
  <Select.Popover>
    <ListBox>
      <ListBox.Item id="cat" textValue="Cat">
        Cat
        <ListBox.ItemIndicator />
      </ListBox.Item>
      <ListBox.Item id="dog" textValue="Dog">
        Dog
        <ListBox.ItemIndicator />
      </ListBox.Item>
      <ListBox.Item id="parrot" textValue="Parrot">
        Parrot
        <ListBox.ItemIndicator />
      </ListBox.Item>
    </ListBox>
  </Select.Popover>
</Select>

Required

import { Select, SelectItem } from "@heroui/react";

<Select isRequired label="Select animal">
  <SelectItem key="cat">Cat</SelectItem>
  <SelectItem key="dog">Dog</SelectItem>
</Select>
import { Select, Label, ListBox } from "@heroui/react";

<Select isRequired placeholder="Choose one">
  <Label>Select animal</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator />
  </Select.Trigger>
  <Select.Popover>
    <ListBox>
      <ListBox.Item id="cat" textValue="Cat">
        Cat
        <ListBox.ItemIndicator />
      </ListBox.Item>
      <ListBox.Item id="dog" textValue="Dog">
        Dog
        <ListBox.ItemIndicator />
      </ListBox.Item>
    </ListBox>
  </Select.Popover>
</Select>

Custom Indicator

<Select selectorIcon={<CustomIcon />} label="Select animal">
  <SelectItem key="cat">Cat</SelectItem>
</Select>
<Select placeholder="Choose one">
  <Label>Select animal</Label>
  <Select.Trigger>
    <Select.Value />
    <Select.Indicator>
      <CustomIcon />
    </Select.Indicator>
  </Select.Trigger>
  <Select.Popover>
    <ListBox>
      <ListBox.Item id="cat" textValue="Cat">
        Cat
        <ListBox.ItemIndicator />
      </ListBox.Item>
    </ListBox>
  </Select.Popover>
</Select>

Component Anatomy

The v3 Select follows this structure:

Select (Root)
  ├── Label (optional)
  ├── Select.Trigger
  │   ├── Select.Value
  │   └── Select.Indicator
  ├── Description (optional)
  ├── Select.Popover
  │   └── ListBox
  │       ├── ListBox.Item
  │       │   ├── Label (optional)
  │       │   ├── Description (optional)
  │       │   └── ListBox.ItemIndicator
  │       ├── ListBox.Section (optional)
  │       │   ├── Header (section title)
  │       │   └── ListBox.Item
  │       └── Separator (optional, between sections)
  └── FieldError (optional)

Important Notes

Item Identification

  • v2: React's key was used for both list reconciliation and item identity for selection.
  • v3: Use id for state/focus and textValue for accessibility on ListBox.Item; keep React's key for list reconciliation.

Selection Value Types

  • v2: selectedKeys was a Set<Key> or array
  • v3: value is Key | null for single selection or Key[] for multiple selection

Clear Button

The isClearable prop has been removed. To implement a clear button:

<Select value={value} onChange={setValue}>
  <Select.Trigger>
    <Select.Value />
    {value && (
      <button onClick={() => setValue(null)}>Clear</button>
    )}
    <Select.Indicator />
  </Select.Trigger>
  {/* ... */}
</Select>

Summary

  1. Component Structure: Must use compound components (Select.Trigger, Select.Value, Select.Indicator, Select.Popover)
  2. Item Components: SelectItemListBox.Item, SelectSectionListBox.Section (use Header for section titles and Separator between sections)
  3. Label/Description/Error: Use separate components instead of props
  4. Selection Props: selectedKeys/onSelectionChangevalue/onChange
  5. Controlled Open State: New isOpen, defaultOpen, and onOpenChange props for controlling the popover open state
  6. Disabled Options: disabledKeys prop still supported for disabling specific items
  7. Required: isRequired prop still supported for marking the field as required
  8. Styling: variant simplified to primary | secondary; color, size, radius removed - use Tailwind for more
  9. ClassNames Removed: Use className props on individual components
  10. Content Props Removed: startContent, endContent - customize trigger directly
  11. Clear Button: isClearable removed - implement manually
  12. Custom Value: Use Select.Value render prop instead of renderValue

On this page