Select

A select component for choosing from a list of options. Includes both native and custom styled variants.

Installation

Terminal
ReactTailwind
TSX
npx @uiblox/cli add select

Visual Variations

Toggle between three different visual styles: default (bordered), filled (background), and underline (minimal).

Standard select dropdown

Some options disabled

Entire select disabled

Basic Usage

Copy & paste ready
import { Select } from "@/components/ui";

const countries = [
  { value: "us", label: "United States" },
  { value: "uk", label: "United Kingdom" },
  { value: "ca", label: "Canada" },
];

const [value, setValue] = useState("");

<Select
  options={countries}
  value={value}
  onValueChange={setValue}
  placeholder="Select a country..."
/>

With Default Value

Copy & paste ready
<Select
  options={frameworks}
  defaultValue="react"
/>

Disabled State

Copy & paste ready
<Select
  options={frameworks}
  defaultValue="react"
  disabled
/>

Custom Select

A custom dropdown-based select for more styling control.

Copy & paste ready
import { CustomSelect } from "@/components/ui";

<CustomSelect
  options={frameworks}
  placeholder="Choose framework..."
  onValueChange={(value) => console.log(value)}
/>

With Disabled Options

Copy & paste ready
const options = [
  { value: "free", label: "Free Plan" },
  { value: "pro", label: "Pro Plan" },
  { value: "enterprise", label: "Enterprise", disabled: true },
];

<Select options={options} placeholder="Select plan..." />

Rich Select

A rich select component with support for icons, descriptions, and enhanced styling.

ReactTailwind CSSCopy & paste ready
import { RichSelect } from "@/components/ui";

const options = [
  {
    value: "react",
    label: "React",
    description: "A JavaScript library for building user interfaces",
    icon: <ReactIcon />,
  },
  // ... more options
];

<RichSelect
  options={options}
  placeholder="Choose framework..."
/>

Searchable Rich Select

Add the searchable prop to enable filtering options.

ReactTailwind CSSCopy & paste ready
<RichSelect
  options={options}
  placeholder="Search frameworks..."
  searchable
/>

Grouped Options

Options can be grouped by category using the group property.

ReactTailwind CSSCopy & paste ready
const options = [
  { value: "figma", label: "Figma", description: "Design tool", group: "Design" },
  { value: "vscode", label: "VS Code", description: "Code editor", group: "Development" },
  // ... more options
];

<RichSelect
  options={options}
  placeholder="Select tool..."
  searchable
/>

Sizes

Rich Select comes in three sizes: sm, md (default), and lg.

ReactTailwind CSSCopy & paste ready
<RichSelect size="sm" options={options} placeholder="Small" />
<RichSelect size="md" options={options} placeholder="Medium" />
<RichSelect size="lg" options={options} placeholder="Large" />

Accessibility

Select components require careful attention to accessibility. The native Select uses browser defaults, while CustomSelect and RichSelect implement full keyboard navigation and ARIA patterns.

Keyboard Support

  • Enter/Space - Open dropdown / select option
  • Arrow Up/Down - Navigate options
  • Escape - Close dropdown
  • Tab - Move focus out
  • Type ahead - Jump to matching option (searchable)

ARIA Attributes

  • aria-expanded - Dropdown open state
  • aria-selected - Current selection
  • aria-haspopup - Indicates popup

Accessible Select Patterns

// Always include a label
<div className="space-y-2">
  <label htmlFor="country" className="text-sm font-medium text-white">
    Country <span className="text-red-400" aria-hidden="true">*</span>
    <span className="sr-only">(required)</span>
  </label>
  <Select
    id="country"
    options={countries}
    placeholder="Select a country..."
    aria-required="true"
    aria-describedby="country-hint"
  />
  <p id="country-hint" className="text-sm text-[#94a3b8]">
    Select your country of residence
  </p>
</div>

// Error state
<div className="space-y-2">
  <label htmlFor="plan" className="text-sm font-medium text-white">
    Subscription Plan
  </label>
  <Select
    id="plan"
    options={plans}
    aria-invalid="true"
    aria-describedby="plan-error"
  />
  <p id="plan-error" className="text-sm text-red-400" role="alert">
    Please select a subscription plan to continue.
  </p>
</div>

// Custom Select with accessible label
<div className="space-y-2">
  <label id="framework-label" className="text-sm font-medium text-white">
    Framework
  </label>
  <RichSelect
    options={frameworks}
    placeholder="Choose framework..."
    aria-labelledby="framework-label"
  />
</div>

// Grouped options announce group names
<RichSelect
  options={[
    { value: "figma", label: "Figma", group: "Design" },
    { value: "vscode", label: "VS Code", group: "Development" },
  ]}
  // Screen reader will announce "Design, Figma" when navigating
/>

Best Practices

  • • Always provide a visible label or aria-label
  • • Use the native Select for best screen reader compatibility
  • • Enable searchable for long option lists
  • • Ensure disabled options explain why they're disabled
  • • Test with keyboard-only navigation

Props

PropTypeDefaultDescription
options*{ value: string; label: string; disabled?: boolean }[]-Array of options to display
valuestring-Controlled value
defaultValuestring-Default value for uncontrolled usage
placeholderstring-Placeholder text shown when no value is selected
onValueChange(value: string) => void-Callback when value changes
disabledbooleanfalseWhether the select is disabled

Source Code

Copy this code into src/components/ui/select.tsx:

select.tsx
ReactTailwind
TSX
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

interface SelectOption {
  value: string;
  label: string;
  disabled?: boolean;
}

interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
  options: SelectOption[];
  placeholder?: string;
  onValueChange?: (value: string) => void;
}

const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
  ({ className, options, placeholder, value, defaultValue, onValueChange, disabled, ...props }, ref) => {
    return (
      <div className="relative">
        <select
          ref={ref}
          value={value}
          defaultValue={defaultValue}
          disabled={disabled}
          onChange={(e) => onValueChange?.(e.target.value)}
          className={cn(
            "flex h-10 w-full appearance-none rounded-md border border-gray-300 dark:border-gray-700",
            "bg-white dark:bg-gray-900 px-3 py-2 pr-8 text-sm text-gray-900 dark:text-gray-100",
            "focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2",
            "disabled:cursor-not-allowed disabled:opacity-50",
            className
          )}
          {...props}
        >
          {placeholder && <option value="" disabled>{placeholder}</option>}
          {options.map((option) => (
            <option key={option.value} value={option.value} disabled={option.disabled}>
              {option.label}
            </option>
          ))}
        </select>
        <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
          <svg className="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
          </svg>
        </div>
      </div>
    );
  }
);
Select.displayName = "Select";

export { Select };