Native Select
A styled native select element. Lightweight alternative to custom dropdowns — uses OS-optimized pickers on mobile devices.
AccessibleDark Mode3 SizesMobile OptimizedLightweight
Install
$
npx react-principles add native-select01
Live Demo
Explore all variants and interactive states in Storybook.
Open Storybookopen_in_newChoose the feature category
Category:
(none)Size:
mdFruit:
(none)02
Code Snippet
src/ui/NativeSelect.tsx
import { NativeSelect } from "@/ui/NativeSelect"; function Example() { const [value, setValue] = useState(""); return ( <NativeSelect label="Choose an option" placeholder="Select..." options={[ { label: "Design System", value: "design-system" }, { label: "Cookbook", value: "cookbook" }, { label: "CLI", value: "cli" }, ]} value={value} onChange={(e) => setValue(e.target.value)} /> ); }
03
Copy-Paste (Single File)
NativeSelect.tsx
import { forwardRef, type SelectHTMLAttributes } from "react"; import { cn } from "@/lib/utils"; // ─── Types ──────────────────────────────────────────────────────────────────── export type NativeSelectSize = "sm" | "md" | "lg"; export interface NativeSelectOption { label: string; value: string; disabled?: boolean; } export interface NativeSelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, "size"> { label?: string; description?: string; error?: string; size?: NativeSelectSize; options?: NativeSelectOption[]; placeholder?: string; } // ─── Constants ──────────────────────────────────────────────────────────────── const SIZE_CLASSES: Record<NativeSelectSize, string> = { sm: "h-8 px-3 pr-9 text-xs", md: "h-10 px-3.5 pr-10 text-sm", lg: "h-12 px-4 pr-11 text-base", }; const BASE_CLASSES = "appearance-none w-full rounded-lg border border-slate-200 dark:border-[#1f2937] " + "bg-white dark:bg-[#0d1117] " + "text-slate-900 dark:text-white " + "placeholder:text-slate-400 dark:placeholder:text-slate-500 " + "focus:border-primary dark:focus:border-primary " + "focus:outline-none " + "focus:ring-2 focus:ring-primary/20 " + "disabled:opacity-50 disabled:cursor-not-allowed " + "transition-colors"; const ERROR_CLASSES = "border-red-400 dark:border-red-500 " + "focus:border-red-400 dark:focus:border-red-500 " + "focus:ring-red-400/20"; // ─── Component ──────────────────────────────────────────────────────────────── export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>( function NativeSelectRoot( { label, description, error, size = "md", options, placeholder, disabled, className, id, children, ...rest }, ref ) { const selectId = id ?? (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined); const descriptionId = id ? `${id}-description` : undefined; const errorId = id ? `${id}-error` : undefined; return ( <div className={cn("flex flex-col gap-1.5", className)}> {label && ( <label htmlFor={selectId} className={cn("font-medium text-slate-700 dark:text-slate-300 text-sm", disabled && "opacity-50")} > {label} </label> )} <div className="relative"> <select ref={ref} id={selectId} disabled={disabled} aria-describedby={error ? errorId : description ? descriptionId : undefined} aria-invalid={!!error} className={cn(BASE_CLASSES, SIZE_CLASSES[size], error && ERROR_CLASSES)} {...rest} > {placeholder && <option value="" disabled>{placeholder}</option>} {options?.map((option) => ( <option key={option.value} value={option.value} disabled={option.disabled}> {option.label} </option> ))} {children} </select> {/* Dropdown Icon */} <svg className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-slate-500 dark:text-slate-400" width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" > <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> </div> {description && !error && <p id={descriptionId} className="text-xs text-slate-500 dark:text-slate-400">{description}</p>} {error && <p id={errorId} className="text-xs text-red-500 dark:text-red-400">{error}</p>} </div> ); } );
04
Props
Extends all native HTMLSelectElement attributes (except size).
| Prop | Type | Default | Description |
|---|---|---|---|
options | NativeSelectOption[] | — | Array of { label, value, disabled? } for programmatic rendering. |
placeholder | string | — | Text for empty first option (renders as disabled). |
size | "sm" | "md" | "lg" | "md" | Controls select height and text size. |
error | string | — | Error message — turns border red and replaces description. |
disabled | boolean | false | Disables interaction and reduces opacity. |
label | string | — | Label text displayed above the select. |
description | string | — | Helper text displayed below the select (hidden when error present). |