Qure UI
Components

Select

A control for choosing from a known, closed list — statuses, protocols, modalities.

'use client'import {  Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/registry/qure/ui/select'/** * `items` is what lets the closed trigger show "Preliminary" rather than the * raw value "preliminary" — the popup is not mounted, so Select.Value has * nowhere else to read a label from. */const statuses = {  unread: 'Unread',  'in-progress': 'In progress',  preliminary: 'Preliminary',  final: 'Final',  addendum: 'Addendum',}export default function SelectDemo() {  return (    <Select items={statuses} defaultValue="preliminary">      <SelectTrigger className="w-56" aria-label="Report status">        <SelectValue placeholder="Select a status" />      </SelectTrigger>      <SelectContent>        {Object.entries(statuses).map(([value, label]) => (          <SelectItem key={value} value={value}>{label}</SelectItem>        ))}      </SelectContent>    </Select>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/select.json

Usage

import {
  Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select'
const statuses = { preliminary: 'Preliminary', final: 'Final' }

<Select items={statuses} defaultValue="preliminary">
  <SelectTrigger className="w-56" aria-label="Report status">
    <SelectValue placeholder="Select a status" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="preliminary">Preliminary</SelectItem>
    <SelectItem value="final">Final</SelectItem>
  </SelectContent>
</Select>

Pass items whenever the select starts with a value. The popup is not mounted until it opens, so SelectValue has no labels to look up and shows the raw value — a trigger reading in-progress rather than In progress. items takes the same map you can render the list from, so the two cannot drift apart.

Reach for a select when the list is closed and the reader is not expected to know it by heart. Two or three mutually exclusive options are usually better as a RadioGroup, where all of them are visible at once. A list long enough to need searching wants a combobox, not this.

Composition

<Select>                {/* owns the value and the open state */}
  <SelectTrigger>       {/* the button; renders the chevron itself */}
    <SelectValue>       {/* the chosen label, or the placeholder */}
  </SelectTrigger>
  <SelectContent>       {/* portal + positioner + popup + list */}
    <SelectGroup>       {/* an optional section */}
      <SelectLabel>     {/* its heading, tied to the group by aria */}
      <SelectItem>      {/* one option; renders its own tick */}
    </SelectGroup>
    <SelectSeparator>   {/* a rule between groups */}
  </SelectContent>
</Select>

SelectTrigger adds its own Select.Icon, and SelectItem its own Select.ItemText and Select.ItemIndicator, so the common case stays three levels deep rather than six. SelectContent bundles the portal, the positioner and the two scroll arrows that appear when the list is taller than the space beneath the trigger.

The value lives on the root. defaultValue for uncontrolled, value plus onValueChange for controlled — and name if the select is inside a form, which renders a hidden input.

Sizes

'use client'import {  Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/registry/qure/ui/select'const sizes = ['sm', 'md', 'lg'] as constconst modalities = { cxr: 'Chest X-ray', ct: 'CT', mri: 'MRI' }export default function SelectSize() {  return (    <div className="flex w-full max-w-64 flex-col gap-3">      {sizes.map(size => (        <Select key={size} items={modalities} defaultValue="ct">          <SelectTrigger size={size} aria-label={`Modality, ${size}`}>            <SelectValue />          </SelectTrigger>          <SelectContent>            {Object.entries(modalities).map(([value, label]) => (              <SelectItem key={value} value={value}>{label}</SelectItem>            ))}          </SelectContent>        </Select>      ))}    </div>  )}

The same 32/40/48 scale as Input, so a select and a text field on one row line up. md is the default and the right choice unless the row is dense.

Groups and separators

'use client'import {  Select, SelectContent, SelectGroup, SelectItem, SelectLabel,  SelectSeparator, SelectTrigger, SelectValue,} from '@/registry/qure/ui/select'export default function SelectGroups() {  return (    <Select>      <SelectTrigger className="w-64" aria-label="Protocol">        <SelectValue placeholder="Choose a protocol" />      </SelectTrigger>      <SelectContent>        <SelectGroup>          <SelectLabel>Chest</SelectLabel>          <SelectItem value="cxr-pa">Chest X-ray, PA</SelectItem>          <SelectItem value="cxr-lat">Chest X-ray, lateral</SelectItem>          <SelectItem value="ct-thorax">CT thorax, contrast</SelectItem>        </SelectGroup>        <SelectSeparator />        <SelectGroup>          <SelectLabel>Neuro</SelectLabel>          <SelectItem value="ct-head">CT head, non-contrast</SelectItem>          <SelectItem value="ct-angio">CT angiography, circle of Willis</SelectItem>          <SelectItem value="mri-brain" disabled>MRI brain — scanner offline</SelectItem>        </SelectGroup>      </SelectContent>    </Select>  )}

Group when the list has real sections — body part, modality, site. A disabled item still says why it cannot be picked, which is more use to the reader than an option that has silently vanished.

In a field

Locked once the report is signed.

'use client'import { Label } from '@/registry/qure/ui/label'import {  Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/registry/qure/ui/select'export default function SelectState() {  return (    <div className="flex w-full max-w-64 flex-col gap-5">      <div className="flex flex-col gap-2">        <Label htmlFor="select-priority">Priority</Label>        <Select>          <SelectTrigger id="select-priority">            <SelectValue placeholder="Not set" />          </SelectTrigger>          <SelectContent>            <SelectItem value="routine">Routine</SelectItem>            <SelectItem value="urgent">Urgent</SelectItem>            <SelectItem value="stat">STAT</SelectItem>          </SelectContent>        </Select>      </div>      <div className="flex flex-col gap-2">        <Label htmlFor="select-locked">Reporting radiologist</Label>        <Select items={{ rao: 'Dr A. Rao' }} defaultValue="rao" disabled>          <SelectTrigger id="select-locked">            <SelectValue />          </SelectTrigger>          <SelectContent>            <SelectItem value="rao">Dr A. Rao</SelectItem>          </SelectContent>        </Select>        <p className="text-sm text-muted-foreground">Locked once the report is signed.</p>      </div>    </div>  )}

A placeholder is not a label. "Not set" inside the trigger disappears the moment a value is chosen, and it is never read as the field's name — put a Label above it, or an aria-label on the trigger when the layout genuinely has no room.

The disabled select keeps its value visible rather than emptying: a locked field still has to report what it is locked to.

Choosing several

The popup stays open as findings are ticked, so a list can be built in one pass.

'use client'import * as React from 'react'import {  Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/registry/qure/ui/select'export default function SelectMultiple() {  const [findings, setFindings] = React.useState<string[]>(['nodule', 'effusion'])  return (    <div className="flex w-full max-w-72 flex-col gap-2">      <Select multiple value={findings} onValueChange={setFindings}>        <SelectTrigger aria-label="Findings to include">          <SelectValue placeholder="No findings selected">            {(value: string[]) =>              value.length === 0 ? 'No findings selected' : `${value.length} findings selected`            }          </SelectValue>        </SelectTrigger>        <SelectContent>          <SelectItem value="nodule">Pulmonary nodule</SelectItem>          <SelectItem value="effusion">Pleural effusion</SelectItem>          <SelectItem value="consolidation">Consolidation</SelectItem>          <SelectItem value="cardiomegaly">Cardiomegaly</SelectItem>          <SelectItem value="pneumothorax">Pneumothorax</SelectItem>        </SelectContent>      </Select>      <p className="text-sm text-muted-foreground">        The popup stays open as findings are ticked, so a list can be built in one pass.      </p>    </div>  )}

multiple turns each item into a toggle and leaves the popup open, which is what you want when someone is assembling a list. Pass a function to SelectValue to summarise the selection — without it the trigger shows every label, and five findings will not fit.

How selection reads

Base UI puts data-selected on the chosen item and data-highlighted on the one under the cursor, keyboard or pointer:

.cn-select-item[data-highlighted] { background-color: var(--background-brand-tertiary); }
.cn-select-item[data-selected] { font-weight: 600; }

The Figma popup marks the selected option with weight 600 and nothing more. We keep that and add a tick, because weight alone is only legible next to its neighbours — and the selected row is often the only one on screen.

Keyboard

Space or Enter opens, typing jumps to a matching option, arrows move the highlight, Enter picks it, Escape closes without changing anything.

API Reference

Everything Base UI's Select accepts. What we add on SelectTrigger:

Prop

Type

On SelectContent:

Prop

Type

On Select itself:

Prop

Type

On this page