Select
Data-driven single-value picker on Ark UI's Select machine — pass data, get a styled trigger, dropdown, native form submission, typeahead, and full keyboard / RTL / ARIA.
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.
import { Select } from '@42-central/ui-react/select';
<Select data={['Paris', 'Lyon', 'Nice']} placeholder="Pick a campus" />For a searchable picker reach for Autocomplete; for multiple values, MultiSelect. Both are built on the Combobox primitive.
Playground
Component
Dir
Presets
Props
Code
<Component
placeholder="Pick a project"
clearSectionMode="replace"
size="md"
/>Data shapes
data accepts three styles of shapes:
// 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 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.
Sizes
Five sizes — xs through xl — matching the rest of the family. The trigger reuses the same shell as Input.
Code
<Select size="xs" data={campuses} />
<Select size="sm" data={campuses} />
<Select size="md" data={campuses} />
<Select size="lg" data={campuses} />
<Select size="xl" data={campuses} />Clearable
With clearable, the chevron becomes a clear button once a value is set, and clicking the selected option deselects it.
Start & end slots
startSlot and endSlot place content at the trigger's inline-start / inline-end with the same slot layout as Input. The built-in chevron / clear button live in the end slot, and a consumer endSlot renders just before them.
Code
import { Globe } from 'lucide-react';
<Select
data={['Paris', 'Berlin', 'Tokyo']}
startSlot={<Globe />}
placeholder="Campus"
/>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, 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.
'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
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.
'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
Select wraps itself in 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.
Code
<Select
label="Project"
description="Pick your next evaluation"
data={['Libft', 'ft_printf', 'Get Next Line']}
required
/>
<Select
label="Project"
error="This field is required"
data={['Libft', 'ft_printf', 'Get Next Line']}
/>Or compose Field yourself for full control over the layout — Select picks up the context either way.
Controlled
Drive the value from state. onChange hands you the selected value, or null when cleared.
'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
Select renders a hidden native <select>, so a name wires it into native <form> submission and FormData — no controlled state required.
<form action={save}>
<Select name="project" data={['Libft', 'ft_printf', 'Get Next Line']} />
</form>Translations
translations overrides the accessible names of the chevron and the clear button. The keys are shared by the whole combobox family — Autocomplete, MultiSelect, ComboboxList and MultiComboboxList take the same ones — so one object translates them all. Unset keys keep their English default.
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
Prop
Type
Accessibility
- Full Select keyboard model from Ark:
Space/Enter/↑/↓open and move, type-ahead jumps to matching options,Esccloses,Home/Endjump to first / last. role="listbox"/option,aria-activedescendant,aria-expanded, and the label / description / error associations are all wired (the latter via the built-inField).- A hidden native
<select>backs form submission; passnameto include it. - RTL is honored via the document
dir— the trigger, chevron, and dropdown mirror automatically.
Changelog
- #331
A
translationsprop for the built-in trigger, clear, remove, search, increment/decrement and upload labels
- #265
Keep the visually hidden input contained inside the control
- #251
An outside click no longer dismisses the parent while a nested overlay is open
- #135
Opened inside a
ModalorDrawer, an overlay portals into it, so it always paints above its host
- #77
Field's label names the real control through
aria-labelledby
- #57
Wire the field's description and error to the control with
aria-describedbyandaria-errormessage
The picker family:
Select,MultiSelect,Autocomplete,Combobox,TreeSelectandTreeMultiSelectrenderItemacross the picker family, with a typed item payloadclearSectionModepicks whether the clear button replaces the chevron or sits beside itPickers open on a tap anywhere in the field, and their clear button is keyboard-reachable
Combobox
The kit's composable search-select primitive — a thin styled layer over Ark UI's Combobox. The engine Autocomplete and MultiSelect build on, and the escape hatch for fully custom pickers.
MultiSelect
Pick several values, shown as removable pills in a wrapping, searchable input — the whole family working together (Combobox + PillsInput + Pill).