|Kit

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

Loading preview

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.

Pick your next evaluation
This field is required

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

On this page