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';<Selectdata={['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
<Selectlabel="Project"description="Pick your next evaluation"data={['Libft', 'ft_printf', 'Get Next Line']}required/><Selectlabel="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.
TagsInput
Free-form, creatable tags on Ark UI's TagsInput machine — type to mint a tag, no fixed option list. Editable, paste-aware, with Pill-styled chips.
MultiSelect
Pick several values, shown as removable pills in a wrapping, searchable input — the whole family working together (Combobox + PillsInput + Pill).