# Select

## Summary

Select presents a defined list of options and lets the user pick a single value.
Use it when the choice is one of a known, reasonably short list that lives
inside a form. For longer lists or free-text search, use
[Autocomplete](../Autocomplete/Autocomplete.md); for triggering an action rather than
picking a value, use [Menu](../Menu/Menu.md).

```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>
  );
}
```

## Anatomy

| Part          | Description                                                                                                                         |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Trigger       | The field the user clicks to open the list                                                                                          |
| Label         | Names the field. Sits inside the trigger when empty, floats to a mini-label once a value is chosen. Doubles as the empty-state hint |
| Value         | The selected option, shown inside the trigger                                                                                       |
| Dropdown      | The floating panel of options on large screens                                                                                      |
| Bottom sheet  | The panel of options on small screens (≤490px). Automatic                                                                           |
| Item          | An individual option in the list                                                                                                    |
| Group         | Optional — wraps related items under a shared heading                                                                               |
| Group label   | Optional — the heading shown above a group                                                                                          |
| Separator     | Optional — a divider between items outside of groups                                                                                |
| Description   | Optional — helper text shown beneath the field                                                                                      |
| Error message | Optional — replaces the description while an error is present                                                                       |

## Behaviour

#### Label as placeholder

The `label` names the field and doubles as the empty state. While no value is
selected it sits centered inside the trigger; once the user picks an option it
floats up as a mini-label. 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>
  );
}
```

#### Selected value display

The closed trigger shows the selected `value`, capitalized (e.g. `"active"` →
`"Active"`). It does **not** read the item's children. When the display label
differs from the capitalized value, pass `renderValue` to control what the
trigger shows — otherwise the trigger will silently drift from the list.

```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>
  );
}
```

#### Small screens

On viewports ≤490px the options open as a bottom sheet instead of an anchored
dropdown, matching the pattern used by [Menu](../Menu/Menu.md) and
[Dialog](../Dialog/Dialog.md). This is automatic — the same authored child tree
renders on both. Resize the preview narrow to see the sheet.

#### Controlled only

Select is controlled: always pass `value` and `onValueChange`. There is no
uncontrolled `defaultValue`. The change prop is named `onValueChange` (not
`onChange`) to match the Base UI foundation and to encode the payload — the
value, not an event — in its name. See [Content guidelines](#content-guidelines)
for how this affects form-library wiring.

#### Grouping

Organize related options under section headers with `Select.Group` and
`Select.GroupLabel`. Adjacent groups are divided automatically, so you don't
need a `Select.Separator` between them — reach for `Select.Separator` only to
divide items that sit outside groups.

```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>
  );
}
```

#### States

* **Description** — pass `description` to add supporting help text beneath the
  field.
* **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.
* **Invalid without a message** — use `invalid` on its own to apply the invalid
  styling without a message, for example when a form library renders the error
  text itself.
* **Disabled** — set `disabled` to prevent interaction with 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>
  );
}
```

```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>
  );
}
```

```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>
  );
}
```

## Variants

### Sizes

Use `size="small"` for tighter layouts. The floating mini-label is hidden at the
small size, so the compact control keeps a single-line height. Prefer the
default (large) size in forms so the label remains visible after selection.

```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, so pair it with validation that
lives elsewhere in the surrounding sentence or paragraph.

```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>
  );
}
```

## Content guidelines

Option labels are scanned quickly while the list is open and then read back as
the chosen value once it's closed so they have to work in both places. And
because the `label` prop doubles as the empty-state hint, it has to work as a
field name and as an invitation to pick.

#### Sentence case

Option labels and group labels are sentence-cased. Capitalize only the first
letter unless there is a proper noun (a person's name, a brand). Jobber features
like jobs, quotes, and invoices are not proper nouns.

| ✅ Do              | ❌ Don't           |
| ----------------- | ----------------- |
| Credit/debit card | Credit/Debit Card |
| Bank transfer     | BANK TRANSFER     |
| Authorize.net     | authorize.net     |
| Jasmine Williams  | jasmine williams  |

#### Noun-first labels

Select options represent things the user is picking, not actions they're taking.
Lead with the noun. If a label reads like an imperative verb phrase, reach for
[Menu](../Menu/Menu.md) or a set of buttons instead.

| ✅ Do    | ❌ Don't              |
| ------- | -------------------- |
| Cash    | Paid by cash         |
| Draft   | Save as draft        |
| Overdue | Show overdue only    |
| Active  | Set status to active |

#### Keep labels short and parallel

Aim for 1–3 words. Every item in the same list should follow the same
grammatical shape.

| ✅ Do                               | ❌ Don't                                              |
| ---------------------------------- | ---------------------------------------------------- |
| Small / Medium / Large             | Small / 12px / Large                                 |
| Draft / Sent / Paid                | Draft / Sent to client / Payment received            |
| Bank transfer / Credit card / Cash | Bank transfer / Charge to credit card / Cash on hand |

#### The `label` prop names the field and is the empty state

The `label` is both the field heading and, before a value is picked, the
placeholder inside the trigger. Use a noun or short noun phrase (1–3 words).
Don't include the word "Select" or "Choose"; the field itself is the invitation.

| ✅ Do           | ❌ Don't                     |
| -------------- | --------------------------- |
| Payment method | Select a payment method     |
| Status         | Choose a status             |
| Team member    | Please assign a team member |

#### Don't repeat the label in every option

The `label` already names the category. Repeating it in each option adds noise
and eats horizontal space in the trigger. With a `label` of "Payment method":

| ✅ Do       | ❌ Don't               |
| ---------- | --------------------- |
| Cash       | Cash payment method   |
| Cheque     | Cheque payment method |
| E-transfer | Payment by e-transfer |

#### Group labels describe, don't instruct

Keep group labels to 1–2 words that name the category. Don't phrase them as
prompts to the user.

| ✅ Do            | ❌ Don't                   |
| --------------- | ------------------------- |
| Jobber Payments | Payments through Jobber   |
| Manual entry    | Record a payment manually |
| Active          | Currently active statuses |

#### Use `renderValue` when the trigger should differ from the item

By default the trigger shows the selected `value` capitalized. Pass
`renderValue` when the item label carries detail the trigger doesn't need, when
the underlying value is a raw enum, or when items include an icon or adornment.
The item shows what the user is choosing between; the trigger
shows what they chose.

| Scenario                        | Item label                    | `renderValue` returns |
| ------------------------------- | ----------------------------- | --------------------- |
| Item has qualifying detail      | `High — needs response today` | `High`                |
| Item includes an icon adornment | `🔒 Private`                  | `Private`             |

## Do's and Don'ts

#### Do:

* ✅ Use sentence case for option and group labels
* ✅ Pass `renderValue` whenever the item label differs from the capitalized
  `value`
* ✅ Wire `value`, `onValueChange`, `onBlur`, and `ref` explicitly when using a
  form library
* ✅ Group options with `Select.Group` + `Select.GroupLabel` when they fall into
  two or more categories
* ✅ Reach for [Autocomplete](../Autocomplete/Autocomplete.md) instead when the list is
  long or benefits from typeahead

#### Don't:

* ❌ Don't spread a React Hook Form `field` object into Select — `onValueChange`
  won't wire from `field.onChange` and the field will silently stop updating
* ❌ Don't include "Select" or "Choose" in the `label` — the field itself is the
  invitation
* ❌ Don't mix noun options and verb options in the same list; if the items are
  actions, use [Menu](../Menu/Menu.md) instead
* ❌ Don't repeat the field's label as a prefix in every option
* ❌ Don't assume the trigger shows the item's children — it shows the `value`,
  capitalized, unless you pass `renderValue`

## Accessibility

Select is built on Base UI's `Field` and `Select` primitives, which handle label
wiring, ARIA state, and the interaction model.

#### Keyboard navigation

| Key                | Behaviour                                                           |
| ------------------ | ------------------------------------------------------------------- |
| Tab                | Moves focus to the trigger                                          |
| Enter or Space     | Opens the dropdown / bottom sheet                                   |
| Up and Down arrows | Move through the list. Wrap at the ends                             |
| Home / End         | Jump to the first or last option                                    |
| Enter              | Selects the highlighted option and closes                           |
| Esc                | Closes without selecting                                            |
| Type-to-select     | Typing letters jumps to the next option starting with those letters |

#### Screen readers

The `label` is wired to the trigger via `Field.Label`, so screen readers
announce the field's purpose along with the selected value. When `error` or
`invalid` is set, the field is announced as invalid; the `error` message is
associated with the field so it is read after the label.

#### Focus management

Select exposes an imperative `ref.focus()` handle that moves focus to the
trigger. Use it to implement focus-on-error in a form — for example, focusing
the first invalid Select when the SP tries to submit.

#### Touch targets

On small screens (≤490px) the options open as a bottom sheet, so each item gets
the vertical space needed to hit standard touch-target sizing. No per-item
configuration required.

## Related components

* Use [LegacySelect](../LegacySelect/LegacySelect.md) if you are working in an app that
  has not yet migrated off the previous native `<select>`-based implementation.
* Use [Autocomplete](../Autocomplete/Autocomplete.md) when the list is long, needs
  typeahead search, or when the user might type a value that isn't in the list.
* Use [Menu](../Menu/Menu.md) when the choice triggers an action rather than
  picking a value that persists in the form.
* Use [RadioGroup](../RadioGroup/RadioGroup.md) when the set of options is small
  (roughly five or fewer) and it helps the SP to see all of them at once.


## 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.

## 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.


## 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 | — |  |
