---
title: "Select"
description: "FormSelect, FormSelectIcon, Option and SelectPlaceholder, a native select with a placeholder, a token icon and prepend and append slots."
url: https://docs.systhema.app/components/form-select
version: unreleased (main)
docs_index: https://docs.systhema.app/llms.txt
---

`FormSelect` is a native `<select>` on the same input surface as a text field, with a trailing icon in place of the browser's arrow. `Option` and `SelectPlaceholder` fill it. For a labelled select with a description and an error message, use [`SelectField`](https://docs.systhema.app/components/form-fields.md#selectfield).

```tsx preview title="Select with a placeholder"
import { FormGroup, FormLabel, FormSelect, Option, SelectPlaceholder } from '@systhemaui/next'

export default function Demo() {
  return (
    <FormGroup style={{ width: '100%', maxWidth: 480 }}>
      <FormLabel htmlFor="select-country">Country</FormLabel>
      <FormSelect name="select-country" autoComplete="country">
        <SelectPlaceholder>Select a country</SelectPlaceholder>
        <Option value="be">Belgium</Option>
        <Option value="hu">Hungary</Option>
        <Option value="nl">Netherlands</Option>
      </FormSelect>
    </FormGroup>
  )
}
```

## Import

```tsx
import { FormSelect, FormSelectIcon, Option, SelectPlaceholder } from '@systhemaui/next'
```

In a React app without Next.js, import them from `@systhemaui/react`.

## Parts

| Part                | Renders                                                                                |
| ------------------- | -------------------------------------------------------------------------------------- |
| `FormSelect`        | `<div class="form-select-wrapper">` around `<select class="form-select">` and the icon |
| `FormSelectIcon`    | `<div class="form-select-icon">`, the trailing icon                                    |
| `Option`            | `<option>` with a required string `value`                                              |
| `SelectPlaceholder` | `<option value="" hidden disabled>`, the prompt shown before a choice                  |

`className` goes on the wrapper, `selectClassName` on the `<select>` and `iconClassName` on the icon. Every other prop, including `value`, `onChange` and `required`, goes to the `<select>`. `id` defaults to `name`.

## Examples

### Placeholder

`SelectPlaceholder` is an option with `value=""` that is hidden from the list and cannot be picked. An uncontrolled `FormSelect` starts on the empty value, so the placeholder shows until the visitor chooses; while it is selected, the text takes the placeholder color. With `required`, leaving it on the placeholder fails validation.

Every `<option>` prop can be overridden. Turn the placeholder into a selectable "All" entry for a filter by switching `hidden` and `disabled` off:

```tsx preview title="Selectable reset option"
import { FormGroup, FormLabel, FormSelect, Option, SelectPlaceholder } from '@systhemaui/next'

export default function Demo() {
  return (
    <FormGroup style={{ width: '100%', maxWidth: 480 }}>
      <FormLabel htmlFor="select-category">Category</FormLabel>
      <FormSelect name="select-category">
        <SelectPlaceholder hidden={false} disabled={false}>
          All categories
        </SelectPlaceholder>
        <Option value="news">News</Option>
        <Option value="guides">Guides</Option>
        <Option value="releases">Releases</Option>
      </FormSelect>
    </FormGroup>
  )
}
```

### Custom icon

The default icon is an empty `FormSelectIcon` that the CSS masks with `blocks.form.selectIcon` and paints in the input's icon color, so it follows focus and invalid states. Change it for the whole project in [`blocks`](https://docs.systhema.app/styling/blocks.md), or per select: `disableIcon` drops the default, and a `FormSelectIcon` with children in `append` takes its place.

```tsx preview title="Select with its own icon"
import { FormGroup, FormLabel, FormSelect, FormSelectIcon, Option } from '@systhemaui/next'

export default function Demo() {
  return (
    <FormGroup style={{ width: '100%', maxWidth: 480 }}>
      <FormLabel htmlFor="select-sort">Sort by</FormLabel>
      <FormSelect
        name="select-sort"
        defaultValue="newest"
        disableIcon
        append={
          <FormSelectIcon>
            <svg
              viewBox="0 -960 960 960"
              width="100%"
              height="100%"
              fill="currentColor"
              aria-hidden="true"
              focusable="false"
            >
              <path d="M480-345 240-585l56-56 184 184 184-184 56 56-240 240Z" />
            </svg>
          </FormSelectIcon>
        }
      >
        <Option value="newest">Newest first</Option>
        <Option value="oldest">Oldest first</Option>
        <Option value="title">Title</Option>
      </FormSelect>
    </FormGroup>
  )
}
```

`prepend` renders before the `<select>` inside the wrapper, and `append` after the icon. Both are for absolutely positioned decorations; the wrapper is the positioning context.

### States

```tsx preview title="Invalid and disabled"
import { FormGroup, FormLabel, FormSelect, Option, SelectPlaceholder } from '@systhemaui/next'

export default function Demo() {
  return (
    <div style={{ display: 'grid', gap: 24, width: '100%', maxWidth: 480 }}>
      <FormGroup>
        <FormLabel htmlFor="select-size">Size</FormLabel>
        <FormSelect name="select-size" required selectClassName="is-invalid" aria-invalid>
          <SelectPlaceholder>Choose a size</SelectPlaceholder>
          <Option value="s">Small</Option>
          <Option value="m">Medium</Option>
        </FormSelect>
      </FormGroup>
      <FormGroup>
        <FormLabel htmlFor="select-plan">Plan</FormLabel>
        <FormSelect name="select-plan" defaultValue="team" disabled>
          <Option value="team">Team</Option>
        </FormSelect>
      </FormGroup>
    </div>
  )
}
```

## Props

### `FormSelect`

<!-- generated:props @systhemaui/react FormSelectProps -->

| Prop                           | Type        | Default | Description |
| ------------------------------ | ----------- | ------- | ----------- |
| `name` (required)              | `string`    | -       |             |
| `id`                           | `string`    | -       |             |
| `className`                    | `string`    | -       |             |
| `selectClassName`              | `string`    | -       |             |
| `iconClassName`                | `string`    | -       |             |
| `children` (required)          | `ReactNode` | -       |             |
| `prepend`                      | `ReactNode` | -       |             |
| `append`                       | `ReactNode` | -       |             |
| `disableIcon`                  | `boolean`   | `false` |             |
| …and all `<select>` attributes |             |         |             |

<!-- /generated -->

### `FormSelectIcon`

Takes every `<div>` attribute. Leave it empty for the masked token icon, or pass an icon as children.

### `Option`

<!-- generated:props @systhemaui/react OptionProps -->

| Prop                           | Type     | Default | Description |
| ------------------------------ | -------- | ------- | ----------- |
| `value` (required)             | `string` | -       |             |
| …and all `<option>` attributes |          |         |             |

<!-- /generated -->

### `SelectPlaceholder`

<!-- generated:props @systhemaui/react SelectPlaceholderProps -->

| Prop                           | Type        | Default | Description |
| ------------------------------ | ----------- | ------- | ----------- |
| `children` (required)          | `ReactNode` | -       |             |
| …and all `<option>` attributes |             |         |             |

<!-- /generated -->

## HTML and CSS

```html
<div class="form-group">
  <label class="form-label" for="country">Country</label>
  <div class="form-select-wrapper">
    <select class="form-select" id="country" name="country">
      <option value="" hidden disabled selected>Select a country</option>
      <option value="be">Belgium</option>
      <option value="hu">Hungary</option>
    </select>
    <div class="form-select-icon"></div>
  </div>
</div>
```

The icon must follow the `<select>` as a sibling, because the CSS positions it with `.form-select ~ .form-select-icon`. The placeholder color applies while an empty-value option is the selected one.

<!-- generated:utilities form -->

| Class                     | Styles                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `form`                    | `& > :not([hidden], input[type="hidden"]) ~ :not([hidden], input[type="hidden"]) { margin-top: var(--form-row-gap-y); }`                                                                                                                                                                                                                                                                                                                                                                                                        |
| `form-row`                | `@media (width < 640px) { & { display: flex; flex-direction: column; row-gap: var(--form-row-gap-y); } }`<br>`@media (width >= 640px) { & { display: flex; flex-wrap: wrap; gap: var(--form-row-gap-x); } }`<br>`@media (width >= 640px) { & > .form-group { flex: 1 1 0%; min-width: 0; } }`<br>`@media (width >= 640px) { & > .form-group[style*="--width"] { flex: 0 0 auto; width: calc(var(--width) - var(--form-row-gap-x) * (1 - var(--width) / 100%)); } }`                                                             |
| `form-group`              | `@media (width >= 640px) { .form-row > .form-group { flex: 1 1 0%; min-width: 0; } }`<br>`@media (width >= 640px) { .form-row > .form-group[style*="--width"] { flex: 0 0 auto; width: calc(var(--width) - var(--form-row-gap-x) * (1 - var(--width) / 100%)); } }`<br>`& > :not([hidden]) ~ :not([hidden]) { margin-top: var(--form-group-gap-y); }`<br>`&:has(:required) .form-label::after { content: ' *'; color: var(--color-form-label-required); }`<br>+ 1 more rule                                                     |
| `form-label`              | `& { display: block; color: var(--color-form-label); }`<br>`& { font-family: var(--font-form-label-font-family); font-size: var(--typography-form-label-font-size); font-weight: var(--font-form-label-font-weight); letter-spacing: var(--typography-form-label-letter-spacing); line-height: var(--typography-form-label-line-height); text-transform: none; text-decoration: none; font-style: var(--font-form-label-font-style); }`<br>+ 1 more rule                                                                        |
| `form-description`        | `& { display: block; color: var(--color-form-description); }`<br>`& { font-family: var(--font-form-description-font-family); font-size: var(--typography-form-description-font-size); font-weight: var(--font-form-description-font-weight); letter-spacing: var(--typography-form-description-letter-spacing); line-height: var(--typography-form-description-line-height); text-transform: none; text-decoration: none; font-style: var(--font-form-description-font-style); }`                                               |
| `form-input`              | `& { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…`<br>+ 4 more rules            |
| `form-select`             | `& { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…`<br>+ 12 more rules           |
| `form-textarea`           | `& { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…`<br>+ 5 more rules            |
| `search-field-input`      | `& { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…`<br>+ 9 more rules            |
| `is-invalid`              | `:is(.form-select:user-invalid ~ .form-select-icon, .form-select.is-invalid ~ .form-select-icon) { color: var(--color-form-input-block-invalid-icon); background-color: var(--color-form-input-block-invalid-icon); }`<br>`:is(.form-group:has(:user-invalid) .form-invalid-message, .form-group:has(.is-invalid) .form-invalid-message) { display: block; }`                                                                                                                                                                   |
| `form-select-wrapper`     | `position: relative;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `form-select-icon`        | `:is(.form-select ~ .form-select-icon) { pointer-events: none; position: absolute; top: 50%; transform: translateY(-50%); object-fit: contain; object-position: center; overflow: visible; line-height: 100%; right: var(--form-input-block-padding-x); width: var(--form-input-block-icon-size); height: var(--form-input-block-icon-size); font-size: var(--form-input-block-icon-size); color: var(--color-form-input-block-default-icon); }`<br>+ 3 more rules                                                              |
| `search-field`            | `position: relative;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `search-field-icon`       | `:is(.search-field-input ~ .search-field-icon) { pointer-events: none; position: absolute; top: 50%; transform: translateY(-50%); object-fit: contain; object-position: center; overflow: visible; line-height: 100%; right: var(--form-input-block-padding-x); width: var(--form-input-block-icon-size); height: var(--form-input-block-icon-size); font-size: var(--form-input-block-icon-size); color: var(--color-form-input-block-default-icon); }`<br>+ 2 more rules                                                      |
| `form-inline-input-group` | `& > :not([hidden]) ~ :not([hidden]) { margin-top: var(--form-input-inline-space-y); }`                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `form-radio`              | `& { position: relative; user-select: none; }`<br>`& input[type="radio"], .form-radio  input[type="checkbox"], .form-radio:before { content: ""; position: absolute; left: 0; top: calc(var(--typography-form-inline-line-height) / 2); transform: translateY(-50%); width: var(--form-input-inline-input-size); height: var(--form-input-inline-input-size); }`<br>`& input[type="radio"], .form-radio  input[type="checkbox"] { opacity: 0; cursor: pointer; }`<br>`&:has(:checked):after { opacity: 1; }`<br>+ 13 more rules |
| `form-checkbox`           | `& { position: relative; user-select: none; }`<br>`& input[type="radio"], .form-checkbox  input[type="checkbox"], .form-checkbox:before { content: ""; position: absolute; left: 0; top: calc(var(--typography-form-inline-line-height) / 2); transform: translateY(-50%); width: var(--form-input-inline-input-size); height: var(--form-input-inline-input-size); }`<br>`& input[type="radio"], .form-checkbox  input[type="checkbox"] { opacity: 0; cursor: pointer; }`<br>+ 14 more rules                                   |
| `form-invalid-message`    | `& { display: none; color: var(--color-form-invalid-message); }`<br>`:is(.form-group:has(:user-invalid) .form-invalid-message, .form-group:has(.is-invalid) .form-invalid-message) { display: block; }`                                                                                                                                                                                                                                                                                                                         |
| `form-file-input`         | `& { cursor: pointer; padding: 0; height: calc(var(--form-input-block-padding-y) * 2 + var(--typography-form-block-line-height)); display: flex; align-items: center; }`<br>+ 2 more rules                                                                                                                                                                                                                                                                                                                                      |

<!-- /generated -->

## Next.js

`@systhemaui/next` re-exports the select parts from `@systhemaui/react` unchanged.

## Accessibility

- Give every select a label (`FormLabel htmlFor`), or use `SelectField`, which also wires its description and error.
- The default icon is an empty `<div>` with no content, so screen readers skip it. Mark a custom icon `aria-hidden="true"`, as in the example above.
- A hidden placeholder is not announced as an option. Repeat its hint in the label or a description if it carries information.

## Related

- [Form fields](https://docs.systhema.app/components/form-fields.md#selectfield)
- [SearchField](https://docs.systhema.app/components/search-field.md)
- [Component CSS blocks](https://docs.systhema.app/styling/blocks.md)
