# Select

Select presents a defined list of options to choose from. It is a prop-driven,
labelled dropdown wrapped in a field structure that handles the label,
description, and error messaging for you.

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

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

  return (
    <Select label="Status" value={value} onValueChange={setValue}>
      <Select.Item value="active">Active</Select.Item>
      <Select.Item value="archived">Archived</Select.Item>
      <Select.Item value="draft">Draft</Select.Item>
    </Select>
  );
}
```

## Design & usage guidelines

Use Select when a user needs to pick a single value from a known, reasonably
short list of options. Each option is declared with `Select.Item`, where the
`value` is submitted and the children are shown as the label.

Select is controlled: pass the current `value` and an `onValueChange` handler to
keep your state in sync.

## Empty state

The `label` doubles as the placeholder: while no value is selected it sits
centered inside the field, then rises to a floating mini-label once the user
picks an option. There is no separate `placeholder` prop — the label fills that
role.

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

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

  return (
    <Select label="Status" value={value} onValueChange={setValue}>
      <Select.Item value="active">Active</Select.Item>
      <Select.Item value="archived">Archived</Select.Item>
      <Select.Item value="draft">Draft</Select.Item>
    </Select>
  );
}
```

## Description

Use `description` to add supporting help text beneath the field.

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

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

  return (
    <Select
      label="Status"
      description="This controls who can see the record."
      value={value}
      onValueChange={setValue}
    >
      <Select.Item value="active">Active</Select.Item>
      <Select.Item value="archived">Archived</Select.Item>
      <Select.Item value="draft">Draft</Select.Item>
    </Select>
  );
}
```

## States

### Error

Pass an `error` message to mark the field invalid and show the message beneath
the field. The error replaces the description while it is present. Use `invalid`
instead to apply the invalid styling without a message — for example when a form
library such as React Hook Form renders the error text itself.

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

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

  return (
    <Select
      label="Status"
      description="This controls who can see the record."
      error="Please choose a status."
      value={value}
      onValueChange={setValue}
    >
      <Select.Item value="active">Active</Select.Item>
      <Select.Item value="archived">Archived</Select.Item>
      <Select.Item value="draft">Draft</Select.Item>
    </Select>
  );
}
```

### Disabled

Set `disabled` to prevent interaction with the field.

```tsx
import React from "react";
import { Select } from "@jobber/components/Select";

export function SelectDisabledExample() {
  return (
    <Select label="Status" disabled value="active">
      <Select.Item value="active">Active</Select.Item>
      <Select.Item value="archived">Archived</Select.Item>
      <Select.Item value="draft">Draft</Select.Item>
    </Select>
  );
}
```

## Grouping

Organize related options under section headers with `Select.Group` and
`Select.GroupLabel`. Adjacent groups are divided automatically, so you don't
need to add a separator between them. `Select.Separator` is available for adding
a divider elsewhere in the list.

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

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

  return (
    <Select label="Produce" value={value} onValueChange={setValue}>
      <Select.Group>
        <Select.GroupLabel>Fruits</Select.GroupLabel>
        <Select.Item value="apple">Apple</Select.Item>
        <Select.Item value="banana">Banana</Select.Item>
      </Select.Group>
      <Select.Group>
        <Select.GroupLabel>Vegetables</Select.GroupLabel>
        <Select.Item value="carrot">Carrot</Select.Item>
        <Select.Item value="spinach">Spinach</Select.Item>
      </Select.Group>
    </Select>
  );
}
```

## Custom selected value

The closed trigger shows the selected value capitalized by default. Pass
`renderValue` to display something different from the option's label — here the
list shows short labels (`Low`, `Medium`, `High`) while the trigger shows the
full `"High priority"`. The open list always shows each `Select.Item`'s
children.

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

const PRIORITY_LABELS: Record<string, string> = {
  low: "Low priority",
  medium: "Medium priority",
  high: "High priority",
};

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

  return (
    <Select
      label="Priority"
      value={value}
      onValueChange={setValue}
      renderValue={selected => PRIORITY_LABELS[selected]}
    >
      <Select.Item value="low">Low</Select.Item>
      <Select.Item value="medium">Medium</Select.Item>
      <Select.Item value="high">High</Select.Item>
    </Select>
  );
}
```

## Sizes

Use the `size` prop to render a `small` control for tighter layouts. The
floating mini-label is hidden at the `small` size, so the compact control keeps
a single-line height.

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

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

  return (
    <Select label="Status" size="small" value={value} onValueChange={setValue}>
      <Select.Item value="active">Active</Select.Item>
      <Select.Item value="archived">Archived</Select.Item>
      <Select.Item value="draft">Draft</Select.Item>
    </Select>
  );
}
```

## Inline

Set `inline` to embed the Select within a line of text. The description and
error messaging are suppressed in inline mode.

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

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

  return (
    <div>
      Set the record to{" "}
      <Select label="Status" inline value={value} onValueChange={setValue}>
        <Select.Item value="active">Active</Select.Item>
        <Select.Item value="archived">Archived</Select.Item>
        <Select.Item value="draft">Draft</Select.Item>
      </Select>{" "}
      today.
    </div>
  );
}
```

## Mobile

On small (touch-sized) web screens — viewport widths of `490px` or less — the
options open as a bottom sheet instead of an anchored dropdown, matching the
mobile pattern used by `Menu` and `Dialog`. This is automatic and requires no
props; selection, keyboard, and focus behaviour are unchanged. Resize the
preview narrow (or open the examples above on a phone) to see the sheet.


## Content

### Options and labels

Declare each option with `Select.Item`. The `value` prop is what `onValueChange`
reports and what is submitted with a form; the children are the human-readable
label shown in the open list.

### The selected value display

The closed trigger shows the selected **value**, capitalized by default (e.g.
`"active"` → `"Active"`). It does not read the option's children — so when the
label differs from the capitalized value, pass `renderValue` to control what the
trigger shows:

```tsx
<Select
  label="Status"
  value={value}
  onValueChange={setValue}
  renderValue={value => STATUS_LABELS[value]}
>
  {/* Select.Item options */}
</Select>
```

### Grouping

* `Select.Group`: wraps a set of related options.
* `Select.GroupLabel`: the section header text for a group.
* `Select.Separator`: an optional visual divider. Adjacent `Select.Group`s are
  already divided automatically, so reach for this only to divide options
  elsewhere in the list.

## Component customization

Select is intentionally opinionated: it exposes a curated set of props rather
than the full underlying API. The subcomponents (`Select.Item`, `Select.Group`,
`Select.GroupLabel`, `Select.Separator`) accept `className` and `style` for
per-option styling; reach out to UXF if you need behaviour beyond what the props
provide.

### Controlled usage

Select is controlled only. Always pass `value` and `onValueChange`; there is no
uncontrolled `defaultValue`. The `id` and `name` are generated for you and wired
to the label automatically.

The change handler is named `onValueChange` to stay isomorphic with Base UI, and
to encode the payload (the value, not an event) in its name.

### Using with a form library

Select is form-library-agnostic — it exposes plain controlled props, so any
system (React Hook Form, Formik, TanStack Form, or plain `useState`) drives it
by wiring:

* `value` — the current value
* `onValueChange` — the library's value setter
* `onBlur` / `onFocus` — touched state and blur/focus-mode validation
* `error` (message) or `invalid` (styling only) — validation feedback
* `ref` — exposes `focus()`, so a form can focus this field on error

Because the change prop is `onValueChange` (not `onChange`), spreading a field
object whose handler is named `onChange` — for example React Hook Form's `field`
via `<Select {...field} />` — will **not** wire up the change handler.
`field.onChange` lands on a prop nothing reads, and JSX spread does not error,
so the field silently stops updating. Wire the props explicitly instead:

```tsx
<Controller
  control={control}
  name="status"
  render={({ field }) => (
    <Select
      label="Status"
      name={field.name}
      value={field.value}
      onValueChange={field.onChange}
      onBlur={field.onBlur}
      ref={field.ref}
    >
      {/* Select.Item options */}
    </Select>
  )}
/>
```

If you want spread ergonomics, keep the remap in your own app — the design
system intentionally ships no form-library adapter:

```tsx
// app-side helper, not part of @jobber/components
const toValueField = ({ onChange, ...field }) => ({
  ...field,
  onValueChange: onChange,
});

<Select label="Status" {...toValueField(field)}>
  {/* Select.Item options */}
</Select>;
```

> **Note:** Select is built on Base UI's `Field` primitive, but it is not yet
> wired to participate in a surrounding Base UI `<Form>` — the `Form` `errors`
> prop and `Form`-driven focus-on-error do not reach Select. Drive validation
> through the `error` / `invalid` props for now.

## Related components

* [LegacySelect](../LegacySelect/LegacySelect.md): the previous native `<select>`-based
  implementation.


## Props

### Web

#### Select

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | No | — | `Select.Item`, `Select.Group`, `Select.GroupLabel`, and `Select.Separator` that make up the dropdown list. |
| `description` | `ReactNode` | No | — | Helper text rendered beneath the field. |
| `disabled` | `boolean` | No | — | Disables the whole field. |
| `error` | `string` | No | — | Error message. Shows the message beneath the field and applies invalid styling. Replaces the description while present. |
| `id` | `string` | No | — | Id applied to the field's trigger. Auto-generated when omitted. |
| `inline` | `boolean` | No | — | Renders the field inline and suppresses the description / error. |
| `invalid` | `boolean` | No | — | Applies invalid styling without rendering a message. Use when the message is rendered elsewhere (e.g. by React Hook F... |
| `label` | `string` | No | — | Floating label shown inside the field. It sits as the placeholder when no value is selected and rises to a mini-label... |
| `name` | `string` | No | — | Name used for the form field (FormData key, autofill, test selectors). |
| `onBlur` | `() => void` | No | — | Called when the field's trigger loses focus. Useful for touched state and blur-mode validation. |
| `onFocus` | `() => void` | No | — | Called when the field's trigger receives focus. |
| `onValueChange` | `(value: string) => void` | No | — | Called with the newly selected value. Named to match Base UI. Note: React Hook Form's `field.onChange` will not be wi... |
| `ref` | `Ref<SelectRef>` | No | — | Imperative handle exposing `focus()`. |
| `renderValue` | `(value: string) => ReactNode` | No | — | Renders the selected value shown in the closed trigger. When omitted, the value is shown capitalized (e.g. `"active"`... |
| `size` | `"large" | "small"` | No | — | Field size. |
| `value` | `string` | No | — | The controlled selected value. `null` (or `undefined`) when empty. |

#### Select.Group

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | No | — | A `Select.GroupLabel` and the `Select.Item`s that belong to the group. |
| `className` | `string` | No | — |  |
| `style` | `CSSProperties` | No | — |  |

#### Select.GroupLabel

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | No | — | The section heading shown above the group's options. |
| `className` | `string` | No | — |  |
| `style` | `CSSProperties` | No | — |  |

#### Select.Item

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | Yes | — | The display label for the option. |
| `value` | `string` | Yes | — | The value submitted when this option is selected. |
| `className` | `string` | No | — |  |
| `style` | `CSSProperties` | No | — |  |

#### Select.Separator

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `className` | `string` | No | — |  |
| `style` | `CSSProperties` | No | — |  |
