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/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'] },
]}
/>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/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/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>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.
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).