Skip to content
    Style: Wireframe

    Components · Forms

    Select

    A native select in the field style, with groups, a placeholder and disabled options. No JavaScript.

    40% of the 2,073 sites in The UI Index have a form (821 sites).

    Used in UI Pro: Form section

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/select.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/select.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS.

    select.tsx
    import type { SelectHTMLAttributes } from 'react';
    import { cn } from '@/lib/utils';
    import { CHEVRON_PATH, select, type SelectGroup, type SelectOption } from './select.variants';
    
    export type { SelectGroup, SelectOption };
    
    export type SelectProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, 'children'> & {
      options: (SelectOption | SelectGroup)[];
      /** A first, empty option that asks for a choice ("Choose a region"). */
      placeholder?: string;
    };
    
    const isGroup = (o: SelectOption | SelectGroup): o is SelectGroup => 'options' in o;
    
    /** A native <select> in the field style. Use `defaultValue` (uncontrolled) or `value` with `onChange`. */
    export function Select({ options, placeholder, className, defaultValue, value, ...props }: SelectProps) {
      const initial = placeholder && value === undefined && defaultValue === undefined ? '' : defaultValue;
      return (
        <div className={select.root}>
          <select className={cn(select.field, className)} value={value} defaultValue={value === undefined ? initial : undefined} {...props}>
            {placeholder && <option value="" disabled>{placeholder}</option>}
            {options.map((o) =>
              isGroup(o) ? (
                <optgroup key={o.label} label={o.label}>
                  {o.options.map((x) => <option key={x.value} value={x.value} disabled={x.disabled}>{x.label}</option>)}
                </optgroup>
              ) : (
                <option key={o.value} value={o.value} disabled={o.disabled}>{o.label}</option>
              ),
            )}
          </select>
          <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={select.chevron}><path d={CHEVRON_PATH} /></svg>
        </div>
      );
    }
    select.variants.ts
    import { fieldClasses } from '../input/input.variants';
    
    /** A native <select> in the field style: the platform's own list, keyboard and screen reader support, no JavaScript.
     *  The chevron is drawn over the right padding and ignores the pointer. */
    export const select = {
      root: 'relative w-full',
      // While the empty placeholder option is chosen, the field reads as a placeholder (muted), not as a value.
      field: `${fieldClasses} h-9 cursor-pointer appearance-none py-1 pr-9 has-[option[value='']:checked]:text-muted-foreground [&_option]:text-foreground`,
      chevron: 'pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground',
    };
    export const CHEVRON_PATH = 'M4 6l4 4 4-4';
    
    export type SelectOption = { value: string; label: string; disabled?: boolean };
    export type SelectGroup = { label: string; options: SelectOption[] };