# Select (/docs/select)





`Select` is the "just pass `data`" single-value picker. Give it an array and it renders a styled trigger, a dropdown of options, a hidden native `<select>` for forms, type-ahead, and the full keyboard / RTL / ARIA model from Ark UI's Select machine. Label, description, and error are built in via [`Field`](/docs/field).

```tsx
import { Select } from '@42-central/ui-react/select';

<Select data={['Paris', 'Lyon', 'Nice']} placeholder="Pick a campus" />
```

For a **searchable** picker reach for [`Autocomplete`](/docs/autocomplete); for **multiple** values, [`MultiSelect`](/docs/multi-select). Both are built on the [`Combobox`](/docs/combobox) primitive.

## Playground [#playground]

<story.Playground />

## Data shapes [#data-shapes]

`data` accepts three styles of shapes:

```tsx
// 1 — strings (value === label)
<Select data={['Paris', 'Lyon', 'Nice']} />

// 2 — objects, with optional per-item disabled
<Select
  data={[
    { value: 'paris', label: 'Paris' },
    { value: 'angouleme', label: 'Angoulême', disabled: true },
  ]}
/>

// 3 — groups
<Select
  data={[
    { group: 'France', items: ['Paris', 'Lyon'] },
    { group: 'Europe', items: ['Berlin', 'Madrid'] },
  ]}
/>
```

Each item also accepts an optional `testId`, for a stable `data-testid` on that option regardless of its label — see the [Testing](/docs/testing) guide. It defaults to `` `item-${value}` `` when omitted, and Ark's own `data-value={value}` is always present as a free, complementary selector either way.

<story.Preview variant="Grouped" />

## Sizes [#sizes]

Five sizes — `xs` through `xl` — matching the rest of the family. The trigger reuses the same shell as [`Input`](/docs/input).

<Preview align="block" code="&#x22;<Select size=\&#x22;xs\&#x22; data={campuses} />\n<Select size=\&#x22;sm\&#x22; data={campuses} />\n<Select size=\&#x22;md\&#x22; data={campuses} />\n<Select size=\&#x22;lg\&#x22; data={campuses} />\n<Select size=\&#x22;xl\&#x22; data={campuses} />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-3">
    <Select size="xs" data="['Paris', 'Lyon', 'Nice']" placeholder="xs" />

    <Select size="sm" data="['Paris', 'Lyon', 'Nice']" placeholder="sm" />

    <Select size="md" data="['Paris', 'Lyon', 'Nice']" placeholder="md" />

    <Select size="lg" data="['Paris', 'Lyon', 'Nice']" placeholder="lg" />

    <Select size="xl" data="['Paris', 'Lyon', 'Nice']" placeholder="xl" />
  </div>
</Preview>

## Clearable [#clearable]

With `clearable`, the chevron becomes a clear button once a value is set, and clicking the selected option deselects it.

<story.Preview variant="Clearable" />

## Start & end slots [#start--end-slots]

`startSlot` and `endSlot` place content at the trigger's inline-start / inline-end with the same slot layout as [`Input`](/docs/input). The built-in chevron / clear button live in the end slot, and a consumer `endSlot` renders just before them.

<Preview align="block" code="&#x22;import { Globe } from 'lucide-react';\n\n<Select\n  data={['Paris', 'Berlin', 'Tokyo']}\n  startSlot={<Globe />}\n  placeholder=\&#x22;Campus\&#x22;\n/>&#x22;">
  <div className="w-full max-w-[20rem]">
    <Select data="['Paris', 'Berlin', 'Tokyo']" startSlot="<Globe />" placeholder="Campus" />
  </div>
</Preview>

## Custom rendering [#custom-rendering]

By default each option renders its label plus a check on the selected row. Pass `renderItem` to own the content instead — it receives the (value-narrowed) `item`, including its typed [`payload`](#typed-payload), and the live `{ selected, highlighted }` state, and returns whatever the row should show. `Select` still owns the option box, click, typeahead, and keyboard wiring; you fill the inside. Define the function in a `"use client"` module so it survives the server → client boundary.

<story.Preview variant="Custom rendering" />

```tsx
'use client';
import { Check } from 'lucide-react';

// item.payload is inferred from `data` — here { xp: number } | undefined
const renderProject = (item, { selected }) => (
  <div className="flex w-full items-center gap-2.5">
    <span className="font-mono text-xs text-(--c-solid)">
      {item.label.slice(0, 2).toLowerCase()}
    </span>
    <span className="flex min-w-0 flex-1 flex-col leading-tight">
      <span className="truncate">{item.label}</span>
      {item.payload && (
        <span className="truncate text-xs text-gray-light-500 dark:text-gray-dark-400">
          {item.payload.xp} XP
        </span>
      )}
    </span>
    {selected && <Check className="ms-auto size-4 text-(--c-solid)" />}
  </div>
);

<Select data={projects} renderItem={renderProject} />
```

### Typed payload [#typed-payload]

Attach arbitrary data to an item via `payload` and read it back in `renderItem`. It's inferred from `data`, so `item.payload` is fully typed (and `undefined` for bare-string or payload-less items). The machine ignores it — only your renderer sees it.

```tsx
'use client';
import { Badge } from '@42-central/ui-react/badge';

const projects = [
  { value: 'libft', label: 'Libft', payload: { xp: 420 } },
  { value: 'webserv', label: 'Webserv', payload: { xp: 1340 } },
];

// item.payload is inferred as { xp: number } | undefined
const renderProject = (item) => (
  <span className="flex flex-1 items-center justify-between gap-2">
    {item.label}
    {item.payload && <Badge>{item.payload.xp} XP</Badge>}
  </span>
);

<Select data={projects} renderItem={renderProject} />
```

## Label, description & error [#label-description--error]

`Select` wraps itself in [`Field`](/docs/field), so `label` / `description` / `error` / `required` work out of the box and wire the `aria-*` relationships. Passing `error` flips the trigger to the invalid (red) state.

<Preview align="block" code="&#x22;<Select\n  label=\&#x22;Project\&#x22;\n  description=\&#x22;Pick your next evaluation\&#x22;\n  data={['Libft', 'ft_printf', 'Get Next Line']}\n  required\n/>\n\n<Select\n  label=\&#x22;Project\&#x22;\n  error=\&#x22;This field is required\&#x22;\n  data={['Libft', 'ft_printf', 'Get Next Line']}\n/>&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-5">
    <Select label="Project" description="Pick your next evaluation" data="['Libft', 'ft_printf', 'Get Next Line']" placeholder="Pick one" />

    <Select label="Project" error="This field is required" data="['Libft', 'ft_printf', 'Get Next Line']" placeholder="Pick one" />
  </div>
</Preview>

Or compose `Field` yourself for full control over the layout — `Select` picks up the context either way.

## Controlled [#controlled]

Drive the value from state. `onChange` hands you the selected value, or `null` when cleared.

```tsx
'use client';
import { useState } from 'react';
import { Select } from '@42-central/ui-react/select';

export function ProjectSelect() {
  const [value, setValue] = useState<string | null>(null);
  return (
    <Select
      data={['Libft', 'ft_printf', 'Get Next Line']}
      value={value}
      onChange={setValue}
      clearable
    />
  );
}
```

## Forms [#forms]

`Select` renders a hidden native `<select>`, so a `name` wires it into native `<form>` submission and `FormData` — no controlled state required.

```tsx
<form action={save}>
  <Select name="project" data={['Libft', 'ft_printf', 'Get Next Line']} />
</form>
```

## Translations [#translations]

`translations` overrides the accessible names of the chevron and the clear button. The keys are shared by the whole combobox family — [`Autocomplete`](/docs/autocomplete), [`MultiSelect`](/docs/multi-select), [`ComboboxList`](/docs/combobox-list) and [`MultiComboboxList`](/docs/multi-combobox-list) take the same ones — so one object translates them all. Unset keys keep their English default.

```tsx
const comboboxFr = { triggerLabel: 'Afficher les options', clearTriggerLabel: 'Effacer' };

<Select data={campuses} clearable translations={comboboxFr} />
<MultiSelect data={campuses} clearable translations={{ ...comboboxFr, removeValue: (label) => `Retirer ${label}` }} />
```

## API [#api]

<TypeTable
  type="{
  data: {
    type: 'string[] | SelectItem[] | SelectGroup[]',
    description: 'Options. Strings, { value, label?, disabled? }, or { group, items }.',
  },
  value: {
    type: 'string | null',
    description: 'Controlled value.',
  },
  defaultValue: {
    type: 'string | null',
    description: 'Uncontrolled initial value.',
  },
  onChange: {
    type: '(value: string | null) => void',
    description: 'Fires with the selected value, or null when cleared.',
  },
  renderItem: {
    type: '(item, state) => ReactNode',
    description: 'Render each option yourself. item is { value, label, disabled }; state is { selected, highlighted }. Define it in a &#x22;use client&#x22; module.',
  },
  placeholder: { type: 'string', description: 'Placeholder shown while no value is selected.' },
  clearable: {
    type: 'boolean',
    default: 'false',
    description: 'Show a clear button (and allow deselect) once a value is set.',
  },
  startSlot: { type: 'ReactNode', description: 'Inline-start adornment (icon, label).' },
  endSlot: { type: 'ReactNode', description: 'Inline-end adornment — before the chevron / clear button.' },
  size: { type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;', default: '&#x22;md&#x22;', description: 'Trigger + option sizing.' },
  variant: { type: '&#x22;default&#x22; | &#x22;unstyled&#x22;', default: '&#x22;default&#x22;', description: 'Trigger shell treatment.' },
  color: { type: 'Color', default: '&#x22;brand&#x22;', description: 'Focus-ring accent.' },
  label: { type: 'ReactNode', description: 'Field label.' },
  description: { type: 'ReactNode', description: 'Field helper text.' },
  error: { type: 'ReactNode', description: 'Field error; sets the invalid state.' },
  required: { type: 'boolean', default: 'false', description: 'Required flag + asterisk.' },
  disabled: { type: 'boolean', default: 'false', description: 'Disable the control.' },
  readOnly: { type: 'boolean', default: 'false', description: 'Announce the value but reject changes.' },
  name: { type: 'string', description: 'Name for the hidden select (form submission).' },
  translations: {
    type: '{ triggerLabel?: string; clearTriggerLabel?: string }',
    description: &#x22;Accessible names of the chevron (default \&#x22;Show options\&#x22;) and the clear button (default \&#x22;Clear value\&#x22;) — the same keys across the combobox family (Select, Autocomplete, MultiSelect, ComboboxList, MultiComboboxList), so one translation object covers them all. A partial override keeps the other default.&#x22;,
  },
  classNames: { type: '{ field?, control?, option?, popover? }', description: 'Per-part class overrides.' },
  testId: { type: 'string', description: 'data-testid on the visible trigger button — never the hidden native <select>. See the Testing guide.' },
}"
/>

## Accessibility [#accessibility]

* Full Select keyboard model from Ark: `Space` / `Enter` / `↑` / `↓` open and move, type-ahead jumps to matching options, `Esc` closes, `Home` / `End` jump to first / last.
* `role="listbox"` / `option`, `aria-activedescendant`, `aria-expanded`, and the label / description / error associations are all wired (the latter via the built-in `Field`).
* A hidden native `<select>` backs form submission; pass `name` to include it.
* RTL is honored via the document `dir` — the trigger, chevron, and dropdown mirror automatically.
