# LegacySelect

> **WARNING:** This component was previously named `Select`. It has been renamed to
> `LegacySelect` to make room for a new `Select` implementation.

A LegacySelect is used to present a defined list of options to choose from.

## Design & usage guidelines

Nested within the LegacySelect component, Option defines the options that can be
selected. For grouping options with section headers, use
LegacySelect.OptionGroup.

#### Custom grouped menu and browser support

To enable the enhanced grouped menu styling (bold section headers, edge‑to‑edge
dividers and hover backgrounds), set the `UNSAFE_experimentalStyles` prop on
`LegacySelect`.

* When `UNSAFE_experimentalStyles` is true and the browser supports
  `appearance: base-select` (Chromium 123+), the select renders with the
  customizable grouped menu.
* In unsupported browsers, or when `UNSAFE_experimentalStyles` is not provided,
  the select remains native while still rendering the same `OptionGroup`
  structure.

## States

### Invalid

```tsx
import React, { useState } from "react";
import { LegacySelect, Option } from "@jobber/components/LegacySelect";

export function LegacySelectInvalidExample() {
  const [value, setValue] = useState<string | number | undefined>("");

  return (
    <LegacySelect invalid={true} value={value} onChange={setValue}>
      <Option value="sad">Tony</Option>
      <Option value="old">Steve</Option>
    </LegacySelect>
  );
}
```

### Disabled

```tsx
import React, { useState } from "react";
import { LegacySelect, Option } from "@jobber/components/LegacySelect";

export function LegacySelectDisabledExample() {
  const [value, setValue] = useState<string | number | undefined>("");

  return (
    <LegacySelect disabled={true} value={value} onChange={setValue}>
      <Option value="sad">Tony</Option>
      <Option value="old">Steve</Option>
    </LegacySelect>
  );
}
```

## Option Grouping

Use `LegacySelect.OptionGroup` to organize options with section headers for
better user experience and visual hierarchy. The `label` prop is required as it
provides the section header text.

### Basic Option Groups

```tsx
import React, { useState } from "react";
import { LegacySelect } from "@jobber/components/LegacySelect";

export function LegacySelectCustomOptionGroupsExample() {
  const [value, setValue] = useState<string | number | undefined>("");

  return (
    <LegacySelect
      placeholder="Select an option"
      UNSAFE_experimentalStyles={true}
      value={value}
      onChange={setValue}
    >
      <LegacySelect.OptionGroup label="Team A">
        <LegacySelect.Option value="alice">Alice</LegacySelect.Option>
        <LegacySelect.Option value="bob">Bob</LegacySelect.Option>
        <LegacySelect.Option value="charlie">Charlie</LegacySelect.Option>
      </LegacySelect.OptionGroup>
      <LegacySelect.OptionGroup label="Team B">
        <LegacySelect.Option value="diana">Diana</LegacySelect.Option>
        <LegacySelect.Option value="evan">Evan</LegacySelect.Option>
        <LegacySelect.Option value="frank">Frank</LegacySelect.Option>
      </LegacySelect.OptionGroup>
      <LegacySelect.OptionGroup label="Team C">
        <LegacySelect.Option value="grace">Grace</LegacySelect.Option>
        <LegacySelect.Option value="hector">Hector</LegacySelect.Option>
        <LegacySelect.Option value="isabel">Isabel</LegacySelect.Option>
      </LegacySelect.OptionGroup>
    </LegacySelect>
  );
}
```

### Disabled Option Groups

```tsx
import React, { useState } from "react";
import { LegacySelect } from "@jobber/components/LegacySelect";

export function LegacySelectCustomOptionGroupDisabledExample() {
  const [value, setValue] = useState<string | number | undefined>("");

  return (
    <LegacySelect
      UNSAFE_experimentalStyles
      placeholder="Select an option"
      value={value}
      onChange={setValue}
    >
      <LegacySelect.OptionGroup label="Available Items">
        <LegacySelect.Option value="option1">Option 1</LegacySelect.Option>
        <LegacySelect.Option value="option2">Option 2</LegacySelect.Option>
      </LegacySelect.OptionGroup>
      <LegacySelect.OptionGroup label="Unavailable Items" disabled>
        <LegacySelect.Option value="option3">Option 3</LegacySelect.Option>
        <LegacySelect.Option value="option4">Option 4</LegacySelect.Option>
      </LegacySelect.OptionGroup>
      <LegacySelect.OptionGroup label="More Items">
        <LegacySelect.Option value="option5">Option 5</LegacySelect.Option>
        <LegacySelect.Option value="option6">Option 6</LegacySelect.Option>
      </LegacySelect.OptionGroup>
    </LegacySelect>
  );
}
```


## Configuration

### Custom (Chromium) vs native

LegacySelect is native by default. Opt into the customizable Chrome UI with
`UNSAFE_experimentalStyles`. This enhances grouped menus in Chromium 123+ when
the browser supports `appearance: base-select`.

#### Alignment limitation (custom UI)

When using `UNSAFE_experimentalStyles`, the closed control (what you see as the
displayed value) is still rendered by the browser's picker. As a result,
right/center alignment of the displayed value is not configurable in Chromium's
customizable select.

* Need right/center alignment? Use the native LegacySelect (omit
  `UNSAFE_experimentalStyles`).
* You can still style the grouped dropdown (optgroup/option) in the custom UI,
  but the closed value alignment will remain default.

## Component customization

### Composable usage

LegacySelect supports composition via subcomponents:

* `LegacySelect.Option`: item in the list. Provide `value` and children as the
  label.
* `LegacySelect.OptionGroup`: group options under a `label`. Use with
  `UNSAFE_experimentalStyles` to opt into the customizable grouped menu in
  Chromium.

#### Customization links

* See
  ["A customizable select"](https://developer.chrome.com/blog/a-customizable-select)
  article for what the Chrome UI exposes: `::picker(select)`, option/optgroup
  styling, and transitions.

## UNSAFE\_ props (advanced usage)

General guidance on `UNSAFE_` props can be found in
[Customizing components](../customizing-components/customizing-components.md).

LegacySelect subcomponents allow `UNSAFE_className` and `UNSAFE_style` to target
the native elements:

* `LegacySelect.OptionGroup` maps to `<optgroup>`
* `LegacySelect.Option` maps to `<option>`

Note that native `<select>` UX differs per browser. Use `UNSAFE_` props with
caution and test across environments. In the custom UI (Chromium), only the
dropdown content is stylable; the closed value alignment is not.


## Props

### Web

#### LegacySelect

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `align` | `"center" | "right"` | No | — | Determines the alignment of the text inside the input. |
| `aria-activedescendant` | `string` | No | — | ID of the currently active descendant element. Used for composite widgets like combobox or listbox. @see {@link https... |
| `aria-autocomplete` | `"both" | "inline" | "list" | "none"` | No | — | Indicates the type of autocomplete interaction. @see {@link https://www.w3.org/TR/wai-aria-1.2/#aria-autocomplete} |
| `aria-controls` | `string` | No | — | Indicates the element that controls the current element. @see {@link https://www.w3.org/TR/wai-aria-1.2/#aria-controls} |
| `aria-describedby` | `string` | No | — | Identifies the element (or elements) that describes the object. @see {@link https://www.w3.org/TR/wai-aria-1.2/#aria-... |
| `aria-details` | `string` | No | — | Identifies the element (or elements) that provide a detailed, extended description. @see {@link https://www.w3.org/TR... |
| `aria-expanded` | `Booleanish` | No | — | Indicates whether the element is expanded or collapsed. @see {@link https://www.w3.org/TR/wai-aria-1.2/#aria-expanded} |
| `aria-label` | `string` | No | — | Defines a string value that labels the current element. @see {@link https://www.w3.org/TR/wai-aria-1.2/#aria-label} |
| `aria-labelledby` | `string` | No | — | Identifies the element (or elements) that labels the current element. @see {@link https://www.w3.org/TR/wai-aria-1.2/... |
| `aria-required` | `Booleanish` | No | — | Indicates that user input is required before form submission. @see {@link https://www.w3.org/TR/wai-aria-1.2/#aria-re... |
| `autoComplete` | `string` | No | — | Autocomplete behavior for the input (React casing, string values only). Use standard HTML autocomplete values or "on"... |
| `autoFocus` | `boolean` | No | — | Whether the input should be auto-focused (React casing). |
| `children` | `ReactNode` | No | — | If you need to pass in a children. For example, `<options>` inside `<select>`. |
| `description` | `ReactNode` | No | — | Further description of the input, can be used for a hint. |
| `disabled` | `boolean` | No | — | Whether the input is disabled. |
| `error` | `string` | No | — | Error message to display. This also highlights the field red. |
| `id` | `string` | No | — | The unique identifier for the input element. |
| `inline` | `boolean` | No | — | Adjusts the form field to go inline with content. |
| `inputMode` | `"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url"` | No | — | Input mode hint for virtual keyboards. |
| `inputRef` | `Ref<HTMLSelectElement>` | No | — |  |
| `invalid` | `boolean` | No | — | Highlights the field red to indicate an error. |
| `loading` | `boolean` | No | — | Show a spinner to indicate loading. |
| `name` | `string` | No | — | The name attribute for the input element. |
| `onBlur` | `(event: FocusEvent<HTMLSelectElement, Element>) => void` | No | — | Blur event handler. |
| `onChange` | `(newValue?: string | number) => void` | No | — |  |
| `onEnter` | `(event: KeyboardEvent<Element>) => void` | No | — | @deprecated Use `onKeyDown` or `onKeyUp` instead. |
| `onFocus` | `(event: FocusEvent<HTMLSelectElement, Element>) => void` | No | — | Focus event handler. |
| `pattern` | `string` | No | — | Validation pattern (regex) for the input. |
| `placeholder` | `string` | No | — | Text that appears inside the input when empty and floats above the value as a mini label once the user enters a value... |
| `prefix` | `Affix` | No | — | Adds a prefix label and icon to the field |
| `required` | `boolean` | No | — | Whether the input is required before form submission. |
| `role` | `string` | No | — | Role attribute for accessibility. |
| `size` | `"large" | "small"` | No | — | Adjusts the interface to either have small or large spacing. |
| `suffix` | `{ onClick: () => void; readonly ariaLabel: string; readonly icon: IconNames; readonly label?: string; } | { onClick?: never; ariaLabel?: never; readonly label?: string; readonly icon?: IconNames; }` | No | — | Adds a suffix label and icon with an optional action to the field |
| `tabIndex` | `number` | No | — | Tab index for keyboard navigation. |
| `UNSAFE_experimentalStyles` | `boolean` | No | — | Opt-in to the customizable select UI (Chromium 123+). When true, the component will apply the custom select styles De... |
| `value` | `number | string` | No | — |  |

#### LegacySelect.Option

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `disabled` | `boolean` | No | — |  |
| `value` | `string` | No | — | The content of this attribute represents the value to be submitted with the form, should this option be selected. |

#### LegacySelect.OptionGroup

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `label` | `string` | Yes | — |  |
| `disabled` | `boolean` | No | — |  |
| `UNSAFE_className` | `string` | No | — | Use at your own risk: Custom classnames for specific elements. This should only be used as a last resort. Using this ... |
| `UNSAFE_style` | `{ container?: CSSProperties; }` | No | — | Use at your own risk: Custom style for specific elements. This should only be used as a last resort. Using this may r... |
