Skip to content
Highlight

V2 componentsUpdated Oct 14, 2026

Select

A single-choice picker. Pass items to the root so the trigger can show the selected label and size itself to the widest option.

Source: packages/ui/src/components/select

tsx
import { Select, SelectItemType } from '@highlight/ui'

Change the props; the preview and the snippet update together.

Props
Size
Icons
Hide icon (chevron)
No value
Disabled
tsx
const VISIBILITY = [  { label: 'Private', value: 'private', icon: <Lock /> },  { label: 'Team', value: 'team', icon: <Users /> },  { label: 'Anyone with the link', value: 'public', icon: <Globe /> },] <Select items={VISIBILITY} defaultValue="team">  <Select.Trigger placeholder="Choose…" />  <Select.Content>    <Select.Item value="private" icon={<Lock />}>Private</Select.Item>    <Select.Item value="team" icon={<Users />}>Team</Select.Item>    <Select.Item value="public" icon={<Globe />}>Anyone with the link</Select.Item>  </Select.Content></Select>
tsx
import { Select, type SelectItemType } from '@highlight/ui' const MODELS: Array<SelectItemType> = [  { label: 'Claude Fable 5.1', value: 'fable' },  { label: 'Claude Opus 5', value: 'opus' },  { label: 'Claude Sonnet 5', value: 'sonnet' },  { label: 'Claude Haiku 4.5', value: 'haiku' },] export default function SelectBasic() {  return (    <Select items={MODELS} defaultValue="fable">      <Select.Trigger />      <Select.Content>        {MODELS.map((item) => (          <Select.Item key={String(item.value)} value={item.value}>            {item.label}          </Select.Item>        ))}      </Select.Content>    </Select>  )}

sm, md, lg and xl are the shared field ramp, so a trigger and a button in one row line up.

tsx
import { CONTROL_SHELL_SIZES, Select, type SelectItemType } from '@highlight/ui' const STATUSES: Array<SelectItemType> = [  { label: 'Todo', value: 'todo' },  { label: 'In progress', value: 'in-progress' },  { label: 'Done', value: 'done' },] function StatusItems() {  return (    <Select.Content>      {STATUSES.map((item) => (        <Select.Item key={String(item.value)} value={item.value}>          {item.label}        </Select.Item>      ))}    </Select.Content>  )} export default function SelectSizes() {  return (    <div className="flex flex-wrap items-end justify-center gap-4">      {CONTROL_SHELL_SIZES.map((size) => (        <Select key={size} items={STATUSES} defaultValue="in-progress">          <Select.Trigger size={size} />          <StatusItems />        </Select>      ))}    </div>  )}

An icon on an item shows beside the label in both the list and the trigger. value and onValueChange make it controlled, the same shape as Base UI.

tsx
import { useState } from 'react'import { Globe, Lock, Users } from 'lucide-react'import { Select, type SelectItemType } from '@highlight/ui' const VISIBILITY: Array<SelectItemType> = [  { label: 'Private', value: 'private', icon: <Lock /> },  { label: 'Team', value: 'team', icon: <Users /> },  { label: 'Anyone with the link', value: 'public', icon: <Globe /> },] export default function SelectIcons() {  const [value, setValue] = useState<string | null>('team')   return (    <Select items={VISIBILITY} value={value} onValueChange={(next) => setValue(next as string | null)}>      <Select.Trigger placeholder="Choose…" />      <Select.Content>        {VISIBILITY.map((item) => (          <Select.Item key={String(item.value)} value={item.value} icon={item.icon}>            {item.label}          </Select.Item>        ))}      </Select.Content>    </Select>  )}

Field pairs the label and note with the trigger and generates the wiring. Pass nativeLabel={false} on the label: a trigger is button-like, and a native label hands it hover it should not have.

Visibility

Anyone with the link can read, but not edit.

tsx
import { Field, Select, type SelectItemType } from '@highlight/ui' const VISIBILITY: Array<SelectItemType> = [  { label: 'Private', value: 'private' },  { label: 'Team', value: 'team' },  { label: 'Anyone with the link', value: 'public' },] function VisibilityItems() {  return (    <Select.Content>      {VISIBILITY.map((item) => (        <Select.Item key={String(item.value)} value={item.value}>          {item.label}        </Select.Item>      ))}    </Select.Content>  )} export default function SelectField() {  return (    <div className="flex w-full max-w-md flex-col">      <Field>        {/* nativeLabel={false}: a native label hands its clicks and hover to the control, and on a            trigger that reads as the button lighting up whenever the pointer crosses the words. */}        <Field.Label nativeLabel={false}>Visibility</Field.Label>        <Select items={VISIBILITY} defaultValue="team">          <Select.Trigger />          <VisibilityItems />        </Select>        <Field.Note>Anyone with the link can read, but not edit.</Field.Note>      </Field>    </div>  )}

description adds a second line under an option, the same row Menu.ItemText draws. The trigger still shows only the label and icon.

tsx
import { useState } from 'react'import { Brain, Gauge, Zap } from 'lucide-react'import { Select, type SelectItemType } from '@highlight/ui' const EFFORT: Array<SelectItemType> = [  { label: 'Fast', value: 'low', icon: <Zap />, description: 'Answers right away. Best for quick questions.' },  { label: 'Balanced', value: 'medium', icon: <Gauge />, description: 'Thinks a little before answering.' },  { label: 'Thorough', value: 'high', icon: <Brain />, description: 'Works through hard problems step by step.' },] export default function SelectDescriptions() {  const [value, setValue] = useState<string | null>('medium')   return (    <Select items={EFFORT} value={value} onValueChange={(next) => setValue(next as string | null)}>      <Select.Trigger placeholder="Choose…" />      <Select.Content>        {EFFORT.map((item) => (          <Select.Item key={String(item.value)} value={item.value} icon={item.icon} description={item.description}>            {item.label}          </Select.Item>        ))}      </Select.Content>    </Select>  )}

The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.

Select
Base UI Select.Root. Owns the value and open state; pass items so the trigger can show the chosen label.
Select.Trigger
size: 'sm' | 'md' | 'lg' | 'xl', default 'md'; placeholder; hideIcon; minWidth.
Select.Content
Portal + Positioner + Popup with scroll arrows. showScrollArrows, positionerProps.
Select.Item
value, icon, onClick.
Select.Value / Icon
Base UI parts for custom triggers.

Props added on top of the underlying element or Base UI part.

PropTypeDefaultDescription
itemsArray<SelectItemType>—label, value, optional icon. Drives the trigger label and width.
autoWidthbooleantrueSize the trigger to the widest label.
minWidthstring—Explicit trigger min-width; disables autoWidth.
classNamestring—Applied to the column wrapping the trigger, not the trigger.
  • Captioning is Field’s job. Select has no Label of its own: it was bare text above the trigger that associated with nothing, so clicking it focused nothing and a screen reader never connected the two.
  • Every Base UI Select.Root prop passes through: value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, disabled, readOnly, required, name, multiple, modal.
  • Trigger, Input and Combobox.Input share one controlShellVariants shell, so they cannot drift on height, padding, radius or any state.
  • items is read from context by Trigger, so a custom trigger built from Select.Value gets the same labels without repeating the list.