@hoag/ui — UI components built with shadcn, Radix UI & Tailwind CSS
Components
Select

Select

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

Installation

npx shadcn@latest add select

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>

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>

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)

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