Select
A select component for choosing from a list of options. Includes both native and custom styled variants.
Installation
npx @uiblox/cli add selectVisual 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
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
<Select
options={frameworks}
defaultValue="react"
/>Disabled State
<Select
options={frameworks}
defaultValue="react"
disabled
/>Custom Select
A custom dropdown-based select for more styling control.
import { CustomSelect } from "@/components/ui";
<CustomSelect
options={frameworks}
placeholder="Choose framework..."
onValueChange={(value) => console.log(value)}
/>With Disabled Options
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.
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.
<RichSelect
options={options}
placeholder="Search frameworks..."
searchable
/>Grouped Options
Options can be grouped by category using the group property.
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.
<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 optionArrow Up/Down- Navigate optionsEscape- Close dropdownTab- Move focus outType 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
searchablefor long option lists - • Ensure disabled options explain why they're disabled
- • Test with keyboard-only navigation
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| options* | { value: string; label: string; disabled?: boolean }[] | - | Array of options to display |
| value | string | - | Controlled value |
| defaultValue | string | - | Default value for uncontrolled usage |
| placeholder | string | - | Placeholder text shown when no value is selected |
| onValueChange | (value: string) => void | - | Callback when value changes |
| disabled | boolean | false | Whether the select is disabled |
Source Code
Copy this code into src/components/ui/select.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 };