Components
Forms & Input
Select

Select

A customizable select component that supports single/multiple selections, search functionality, floating labels, and both desktop (popover) and mobile (sheet) experiences.

Installation

1. Install dependencies:

npm install @radix-ui/react-popover class-variance-authority lucide-react

2. Copy the source code:

3. Update the import paths to match your project structure.


Usage

import { useState } from "react";
import {
  Select,
  SelectInputTrigger,
  SelectValue,
  SelectBody,
  SelectContentAutoLayout,
  type Option,
} from "@hoag/ui/select";
 
export default function Demo() {
  const [value, setValue] = useState<Option[]>([]);
 
  const options: Option[] = [
    { name: "React", value: "react" },
    { name: "Vue", value: "vue" },
    { name: "Angular", value: "angular" },
  ];
 
  return (
    <Select value={value} onValueChange={setValue}>
      <SelectInputTrigger placeholder="Select framework">
        <SelectValue />
      </SelectInputTrigger>
      <SelectBody>
        <SelectContentAutoLayout options={options} />
      </SelectBody>
    </Select>
  );
}

Basic Select

const [value, setValue] = useState<Option[]>([]);
const options: Option[] = [
  { name: "React", value: "react" },
  { name: "Vue", value: "vue" },
  { name: "Angular", value: "angular" },
  { name: "Svelte", value: "svelte" },
];
 
<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger placeholder="Select framework">
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout options={options} />
  </SelectBody>
</Select>;

With Label

<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger label="Framework" placeholder="Select framework">
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout options={options} />
  </SelectBody>
</Select>

Required Field

<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger
    label="Country"
    isRequired
    placeholder="Select your country"
  >
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout options={countries} />
  </SelectBody>
</Select>

With Search

Enable search functionality to filter options by passing hasSearch prop.

<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger
    label="Country"
    placeholder="Select country"
    hasRemoveIcon
  >
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout hasSearch options={countries} />
  </SelectBody>
</Select>

Multiple Selection

Allow users to select multiple options with the multiple prop.

<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger
    label="Skills"
    placeholder="Select your skills"
    hasRemoveIcon
  >
    <SelectValue>
      {(value) => {
        if (!value?.length) return "";
        return value.length === 1 ? value[0]?.name : `${value.length} selected`;
      }}
    </SelectValue>
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout hasSearch multiple options={skills} />
  </SelectBody>
</Select>

Trigger as Child

Use SelectTrigger with asChild when you need a fully custom trigger (e.g. button, card, inline control).

import {
  Select,
  SelectTrigger,
  SelectBody,
  SelectContentAutoLayout,
  SelectValue,
} from "@hoag/ui/select";
import { Button } from "@hoag/ui/components/button";
import { ChevronDown } from "lucide-react";
 
<Select value={value} onValueChange={setValue}>
  <SelectTrigger asChild>
    <Button variant="outline" className="w-full justify-between">
      {value.length ? value[0]?.name : "Select with custom trigger"}
      <ChevronDown className="h-4 w-4" />
    </Button>
  </SelectTrigger>
  <SelectBody>
    <SelectContentAutoLayout hasSearch options={options} />
  </SelectBody>
</Select>;

Sizes

Select supports different sizes for the trigger input.

<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger size="default" placeholder="Default size">
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout options={options} />
  </SelectBody>
</Select>
 
<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger size="lg" placeholder="Large size">
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout options={options} />
  </SelectBody>
</Select>

Disabled

<Select value={value} onValueChange={setValue} disabled>
  <SelectInputTrigger label="Disabled Select" placeholder="Cannot interact">
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout options={options} />
  </SelectBody>
</Select>

Error State

<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger
    label="Framework"
    isRequired
    isError
    errorText="Please select a framework"
    placeholder="Select framework"
  >
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout options={frameworks} />
  </SelectBody>
</Select>

With Helper Text

<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger
    label="Newsletter"
    helpText="Choose how often you'd like to receive updates"
    placeholder="Select frequency"
  >
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout options={frequencies} />
  </SelectBody>
</Select>

Options with Description

When options are complex, pass description in each option for richer context inside the dropdown.

const optionsWithDescription: Option[] = [
  {
    name: "Studio Apartment",
    value: "studio",
    description: "Compact layout for singles or couples",
  },
  {
    name: "Penthouse",
    value: "penthouse",
    description: "Premium unit with top-floor views and private amenities",
  },
];
 
<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger placeholder="Choose property type" hasRemoveIcon>
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout hasSearch options={optionsWithDescription} />
  </SelectBody>
</Select>;

Large Dataset (1000+ items)

Select works with large option lists and search-driven filtering.

const largeDataset: Option[] = Array.from({ length: 1000 }, (_, i) => ({
  name: `Dataset Item ${i + 1}`,
  value: `item-${i + 1}`,
}));
 
<Select value={value} onValueChange={setValue}>
  <SelectInputTrigger placeholder="Search through 1000+ options">
    <SelectValue />
  </SelectInputTrigger>
  <SelectBody>
    <SelectContentAutoLayout hasSearch options={largeDataset} />
  </SelectBody>
</Select>;

Select inside Dialog

Select can be used safely inside modal/dialog flows.

import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@hoag/ui/legacy/dialog";
 
<Dialog>
  <DialogTrigger asChild>
    <Button variant="outline">Open dialog with select</Button>
  </DialogTrigger>
  <DialogContent className="sm:max-w-[480px]">
    <DialogHeader>
      <DialogTitle>Profile Settings</DialogTitle>
      <DialogDescription>Choose your country.</DialogDescription>
    </DialogHeader>
    <Select value={value} onValueChange={setValue}>
      <SelectInputTrigger
        label="Country"
        placeholder="Select country"
        hasRemoveIcon
      >
        <SelectValue />
      </SelectInputTrigger>
      <SelectBody>
        <SelectContentAutoLayout hasSearch options={countries} />
      </SelectBody>
    </Select>
  </DialogContent>
</Dialog>;

API Reference

Select Props

PropTypeDefaultDescription
valueOption[]-Controlled selected value(s)
onValueChange(value: Option[]) => void-Callback when value changes
defaultValueOption[]-Default selected value(s)
disabledbooleanfalseDisable the select
openboolean-Controlled open state
onOpenChange(open: boolean) => void-Callback when open state changes
defaultOpenboolean-Default open state

SelectInputTrigger Props

PropTypeDefaultDescription
labelstring-Label text (acts as floating label)
placeholderstring-Placeholder text
isRequiredbooleanfalseShow required indicator (*)
isErrorbooleanfalseError state styling
errorTextstring-Error message to display below input
helpTextstring-Helper text below input
size"default" | "lg""default"Input size
hasRemoveIconbooleanfalseShow clear/remove icon when value selected
enableFloatingLabelbooleantrueEnable floating label animation
disabledbooleanfalseDisable the trigger
addonBeforeReactNode-Icon/content before input
iconReactNode-Custom icon (overrides default caret)

SelectTrigger Props

PropTypeDefaultDescription
asChildbooleanfalseRender a custom trigger element
classNamestring-Additional classes for trigger root
childrenReactNode-Trigger content (button, card, inline control)

SelectContentAutoLayout Props

PropTypeDefaultDescription
optionsOption[]RequiredArray of options to display
hasSearchbooleanfalseEnable search/filter functionality
multiplebooleanfalseEnable multiple selection with checkboxes
searchPlaceholderstring"Search..."Placeholder for search input

Option Type

type Option = {
  name: string; // Display text
  value: string; // Unique value
  description?: string; // Optional description text
  disabled?: boolean; // Disable this option
};

Accessibility

  • ✅ Keyboard navigation support (Arrow keys, Enter, Escape)
  • ✅ ARIA attributes for screen readers
  • ✅ Focus management and trap
  • ✅ Built-in search with keyboard input (when hasSearch enabled)
  • ✅ Responsive: Popover on desktop, Sheet on mobile
  • ✅ Disabled state for both select and individual options
  • ✅ Clear button with hasRemoveIcon prop

Notes

  • Auto Layout: Use SelectContentAutoLayout to automatically render options from an array
  • Responsive Design: Component automatically uses Popover on desktop and Sheet on mobile
  • Search: Enable with hasSearch prop - filters options in real-time
  • Multiple Selection: Use multiple prop - includes checkboxes and custom value display
  • Floating Labels: Labels animate automatically when value is selected (controlled by enableFloatingLabel)
  • State Management: Always use controlled component pattern with value and onValueChange
  • Custom Rendering: Use render prop in SelectValue for custom display logic in multiple selection mode
  • Custom Trigger: Use SelectTrigger asChild when you need button/card-like trigger UI
  • Dialog Compatibility: Select works inside Dialog with search and scroll behaviors

MIT 2026 © @hoag/ui