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-react2. Copy the source code:
- Source in this monorepo:
packages/ui/src/common/data-input/selects/select/ - Place in your project:
components/ui/select/ - Required components: Input, Sheet, Checkbox, List components
- Required utility:
lib/utils.ts(forcnhelper)
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
| Prop | Type | Default | Description |
|---|---|---|---|
value | Option[] | - | Controlled selected value(s) |
onValueChange | (value: Option[]) => void | - | Callback when value changes |
defaultValue | Option[] | - | Default selected value(s) |
disabled | boolean | false | Disable the select |
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Callback when open state changes |
defaultOpen | boolean | - | Default open state |
SelectInputTrigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Label text (acts as floating label) |
placeholder | string | - | Placeholder text |
isRequired | boolean | false | Show required indicator (*) |
isError | boolean | false | Error state styling |
errorText | string | - | Error message to display below input |
helpText | string | - | Helper text below input |
size | "default" | "lg" | "default" | Input size |
hasRemoveIcon | boolean | false | Show clear/remove icon when value selected |
enableFloatingLabel | boolean | true | Enable floating label animation |
disabled | boolean | false | Disable the trigger |
addonBefore | ReactNode | - | Icon/content before input |
icon | ReactNode | - | Custom icon (overrides default caret) |
SelectTrigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render a custom trigger element |
className | string | - | Additional classes for trigger root |
children | ReactNode | - | Trigger content (button, card, inline control) |
SelectContentAutoLayout Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | Option[] | Required | Array of options to display |
hasSearch | boolean | false | Enable search/filter functionality |
multiple | boolean | false | Enable multiple selection with checkboxes |
searchPlaceholder | string | "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
hasSearchenabled) - ✅ Responsive: Popover on desktop, Sheet on mobile
- ✅ Disabled state for both select and individual options
- ✅ Clear button with
hasRemoveIconprop
Notes
- Auto Layout: Use
SelectContentAutoLayoutto automatically render options from an array - Responsive Design: Component automatically uses Popover on desktop and Sheet on mobile
- Search: Enable with
hasSearchprop - filters options in real-time - Multiple Selection: Use
multipleprop - 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
valueandonValueChange - Custom Rendering: Use render prop in
SelectValuefor custom display logic in multiple selection mode - Custom Trigger: Use
SelectTrigger asChildwhen you need button/card-like trigger UI - Dialog Compatibility: Select works inside Dialog with search and scroll behaviors