# Native Select

A styled native select element for choosing one option from a list.

Native Select is an open-source component from suiss/ui for React. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/components/native-select

**React**

**Example**

```tsx
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export default function NativeSelectDemo() {
  return (
    <NativeSelect defaultValue="" aria-label="Fruit">
      <NativeSelectOption value="" disabled>
        Select a fruit
      </NativeSelectOption>
      <NativeSelectOption value="apple">Apple</NativeSelectOption>
      <NativeSelectOption value="banana">Banana</NativeSelectOption>
      <NativeSelectOption value="cherry">Cherry</NativeSelectOption>
      <NativeSelectOption value="grape">Grape</NativeSelectOption>
    </NativeSelect>
  )
}
```

`NativeSelect` wraps the browser's `select` element with the library's control styling and a trailing up-down caret. Add choices with `NativeSelectOption`, and group them under headings with `NativeSelectOptGroup`.

Because it uses the platform picker, it works well on touch devices and in plain HTML forms. Use Select when you need a custom-styled popup, rich option content or a searchable list.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/native-select
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add native-select
```

### Manual

1. Install the dependencies and add the [`cn` helper](https://ui.suiss.app/docs/installation#the-cn-helper):

```bash title="Terminal"
npm install @phosphor-icons/react
```

2. Copy the source into `components/ui/native-select.tsx`:

```tsx title="components/ui/native-select.tsx"
"use client"

import { CaretUpDownIcon } from "@phosphor-icons/react"
import type * as React from "react"
import { cn } from "@/lib/utils"

type NativeSelectProps = Omit<React.ComponentProps<"select">, "size"> & {
  size?: "sm" | "default"
}

function NativeSelect({
  className,
  size = "default",
  ...props
}: NativeSelectProps) {
  return (
    <div
      className={cn(
        "relative w-fit has-[select:disabled]:opacity-50",
        className
      )}
      data-slot="native-select-wrapper"
      data-size={size}
    >
      <select
        data-slot="native-select"
        data-size={size}
        className="h-11 w-full min-w-0 appearance-none rounded-lg border border-transparent bg-secondary py-1 pe-8 ps-3 text-base transition-[color,box-shadow,background-color] outline-none select-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground has-[option[value='']:checked]:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:bg-destructive/5 dark:aria-invalid:bg-destructive/10 data-[size=sm]:h-9 data-[size=sm]:rounded-md"
        {...props}
      />
      <CaretUpDownIcon
        className="pointer-events-none absolute top-1/2 inset-e-2.5 size-4 -translate-y-1/2 text-muted-foreground select-none"
        aria-hidden="true"
        data-slot="native-select-icon"
      />
    </div>
  )
}

function NativeSelectOption({
  className,
  ...props
}: React.ComponentProps<"option">) {
  return (
    <option
      data-slot="native-select-option"
      className={cn("bg-[Canvas] text-[CanvasText]", className)}
      {...props}
    />
  )
}

function NativeSelectOptGroup({
  className,
  ...props
}: React.ComponentProps<"optgroup">) {
  return (
    <optgroup
      data-slot="native-select-optgroup"
      className={cn("bg-[Canvas] text-[CanvasText]", className)}
      {...props}
    />
  )
}

export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
```

3. Update the import paths to match your project.

## Usage

```tsx
import { NativeSelect, NativeSelectOption, NativeSelectOptGroup } from "@/components/ui/native-select"
```

```tsx
<NativeSelect defaultValue="" aria-label="Fruit">
  <NativeSelectOption value="" disabled>
    Select a fruit
  </NativeSelectOption>
  <NativeSelectOption value="apple">Apple</NativeSelectOption>
  <NativeSelectOption value="banana">Banana</NativeSelectOption>
  <NativeSelectOption value="cherry">Cherry</NativeSelectOption>
  <NativeSelectOption value="grape">Grape</NativeSelectOption>
</NativeSelect>
```

All standard `select` attributes, such as `value`, `defaultValue`, `onChange`, `name`, `disabled` and `required`, pass through to the `select` element. `className` is applied to the wrapper, not the `select`.

To show a placeholder, add a first option with `value=""` and `disabled`; while it is selected, the text uses `text-muted-foreground`.

## Examples

### Sizes

The `sm` and `default` sizes side by side.

**Example**

```tsx
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export default function NativeSelectSizes() {
  return (
    <div className="flex items-center gap-3">
      <NativeSelect defaultValue="weekly" size="sm" aria-label="Frequency">
        <NativeSelectOption value="daily">Daily</NativeSelectOption>
        <NativeSelectOption value="weekly">Weekly</NativeSelectOption>
        <NativeSelectOption value="monthly">Monthly</NativeSelectOption>
      </NativeSelect>
      <NativeSelect defaultValue="weekly" size="default" aria-label="Frequency">
        <NativeSelectOption value="daily">Daily</NativeSelectOption>
        <NativeSelectOption value="weekly">Weekly</NativeSelectOption>
        <NativeSelectOption value="monthly">Monthly</NativeSelectOption>
      </NativeSelect>
    </div>
  )
}
```

### With groups

Options organized under labelled `NativeSelectOptGroup` headings.

**Example**

```tsx
import {
  NativeSelect,
  NativeSelectOptGroup,
  NativeSelectOption,
} from "@/components/ui/native-select"

export default function NativeSelectWithGroups() {
  return (
    <NativeSelect defaultValue="berlin" aria-label="City">
      <NativeSelectOptGroup label="Europe">
        <NativeSelectOption value="berlin">Berlin</NativeSelectOption>
        <NativeSelectOption value="lisbon">Lisbon</NativeSelectOption>
        <NativeSelectOption value="zurich">Zurich</NativeSelectOption>
      </NativeSelectOptGroup>
      <NativeSelectOptGroup label="Asia">
        <NativeSelectOption value="seoul">Seoul</NativeSelectOption>
        <NativeSelectOption value="singapore">Singapore</NativeSelectOption>
        <NativeSelectOption value="tokyo">Tokyo</NativeSelectOption>
      </NativeSelectOptGroup>
    </NativeSelect>
  )
}
```

### Disabled

A select with `disabled` set, dimmed to 50% opacity.

**Example**

```tsx
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export default function NativeSelectDisabled() {
  return (
    <NativeSelect disabled defaultValue="" aria-label="Fruit">
      <NativeSelectOption value="" disabled>
        Select a fruit
      </NativeSelectOption>
      <NativeSelectOption value="apple">Apple</NativeSelectOption>
      <NativeSelectOption value="banana">Banana</NativeSelectOption>
      <NativeSelectOption value="cherry">Cherry</NativeSelectOption>
      <NativeSelectOption value="grape">Grape</NativeSelectOption>
    </NativeSelect>
  )
}
```

### Invalid

A select with `aria-invalid` set, shown with the destructive border and ring.

**Example**

```tsx
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

export default function NativeSelectInvalid() {
  return (
    <NativeSelect defaultValue="" aria-invalid aria-label="Fruit">
      <NativeSelectOption value="" disabled>
        Select a fruit
      </NativeSelectOption>
      <NativeSelectOption value="apple">Apple</NativeSelectOption>
      <NativeSelectOption value="banana">Banana</NativeSelectOption>
      <NativeSelectOption value="cherry">Cherry</NativeSelectOption>
      <NativeSelectOption value="grape">Grape</NativeSelectOption>
    </NativeSelect>
  )
}
```

## API Reference

- `NativeSelect`
- `NativeSelectOption`
- `NativeSelectOptGroup`
  - `NativeSelectOption`

### NativeSelect

The wrapper and native `select` element, with a decorative caret icon.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "default"` | `"default"` | The control height. |

Also accepts the props of `NativeSelectProps`.

### NativeSelectOption

A single choice, rendered as an `option` element.

Also accepts the props of `React.ComponentProps<"option">`.

### NativeSelectOptGroup

A labelled group of options, rendered as an `optgroup` element.

Also accepts the props of `React.ComponentProps<"optgroup">`.

## Accessibility

- Renders a native `select`, exposed as a `combobox` with native keyboard and picker behavior.
- Provide an accessible name with a `Label` linked through `htmlFor`, or with `aria-label`.
- `NativeSelectOptGroup` exposes its `label` as a named `group`.
- Set `aria-invalid` to communicate a validation error; it also applies the invalid styling.
- The caret icon is `aria-hidden`.

## Notes

- Styling hooks: `data-slot` values `native-select-wrapper`, `native-select`, `native-select-icon`, `native-select-option` and `native-select-optgroup`. The wrapper and `select` both expose `data-size`.
- The `size` prop accepts `default` (44px tall) or `sm` (36px tall) and replaces the native `size` attribute.
- Options and groups use the `Canvas` and `CanvasText` system colors so the native popup stays legible in light and dark modes.
- Uses the `secondary`, `destructive` and `muted-foreground` colors, and `opacity-50` when disabled.

