Qure UI
Components

Autocomplete

A text field that suggests as you type, without ever refusing what you typed.

Anything can be typed here. The list only suggests.

'use client'import {  Autocomplete, AutocompleteContent, AutocompleteEmpty, AutocompleteInput,  AutocompleteItem, AutocompleteList,} from '@/registry/qure/ui/autocomplete'import { Label } from '@/registry/qure/ui/label'const terms = [  'Ground-glass opacity',  'Ground-glass nodule',  'Consolidation',  'Cavitation',  'Bronchiectasis',  'Interlobular septal thickening',  'Tree-in-bud opacities',  'Pleural thickening',]export default function AutocompleteDemo() {  return (    <div className="flex w-80 flex-col gap-2">      <Label htmlFor="impression">Impression</Label>      <Autocomplete items={terms}>        <AutocompleteInput id="impression" placeholder="Describe the finding" />        <AutocompleteContent>          <AutocompleteEmpty>No suggestion — the text is kept as typed.</AutocompleteEmpty>          <AutocompleteList>            {(term: string) => <AutocompleteItem key={term} value={term}>{term}</AutocompleteItem>}          </AutocompleteList>        </AutocompleteContent>      </Autocomplete>      <p className="text-muted-foreground text-sm">        Anything can be typed here. The list only suggests.      </p>    </div>  )}

Installation

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

Usage

import {
  Autocomplete, AutocompleteContent, AutocompleteEmpty,
  AutocompleteInput, AutocompleteItem, AutocompleteList,
} from '@/components/ui/autocomplete'
const terms = ['Ground-glass opacity', 'Consolidation', 'Cavitation']

<Autocomplete items={terms}>
  <AutocompleteInput placeholder="Describe the finding" />
  <AutocompleteContent>
    <AutocompleteEmpty>No suggestion — the text is kept as typed.</AutocompleteEmpty>
    <AutocompleteList>
      {(term: string) => <AutocompleteItem key={term} value={term}>{term}</AutocompleteItem>}
    </AutocompleteList>
  </AutocompleteContent>
</Autocomplete>

Autocomplete or combobox

They look identical and they are not the same control. The difference is what happens to text that matches nothing.

AutocompleteCombobox
Free textKeptDiscarded on close
Holds a selectionNoYes
Row has a tickNoYes
ValueThe string in the inputThe chosen item

An impression field, a search box and a "reason for study" box are autocompletes: the vocabulary helps, but a radiologist who needs to write something the list has never heard of must be able to. A protocol picker is a combobox: a protocol that is not in the list is a typo, not a new protocol.

If you find yourself reading the chosen item back out of an autocomplete, it was a combobox. Autocomplete's value is the text; there is no selected item to read.

Composition

<Autocomplete>                {/* owns the text and the open state */}
  <AutocompleteInputGroup>    {/* optional; only when the field holds more than the input */}
    <AutocompleteInput>
    <AutocompleteClear>
  </AutocompleteInputGroup>
  <AutocompleteContent>       {/* portal + positioner + popup */}
    <AutocompleteStatus>      {/* announced politely */}
    <AutocompleteEmpty>       {/* announced politely */}
    <AutocompleteList>
      <AutocompleteGroup>
        <AutocompleteGroupLabel>
        <AutocompleteCollection>
      <AutocompleteItem>
  </AutocompleteContent>
</Autocomplete>

The input can stand on its own — it carries the field border itself — so the group is only worth adding when there is a search icon or a clear button to put beside it.

Completing inline

'use client'import {  Autocomplete, AutocompleteContent, AutocompleteEmpty, AutocompleteInput,  AutocompleteItem, AutocompleteList,} from '@/registry/qure/ui/autocomplete'import { Label } from '@/registry/qure/ui/label'const sites = [  'Apollo Hospitals, Chennai',  'Apollo Hospitals, Hyderabad',  'Fortis Memorial, Gurugram',  'Kokilaben Dhirubhai Ambani, Mumbai',  'Manipal Hospital, Bengaluru',  'Narayana Health City, Bengaluru',]export default function AutocompleteInline() {  return (    <div className="flex w-80 flex-col gap-2">      <Label htmlFor="site">Site</Label>      {/*        `mode="both"` filters the list *and* completes the input inline — the        rest of the highlighted name appears selected after the caret, so it is        overwritten by the next keystroke rather than fought with.      */}      <Autocomplete items={sites} mode="both" autoHighlight>        <AutocompleteInput id="site" placeholder="Type 'apo'" />        <AutocompleteContent>          <AutocompleteEmpty>No site by that name.</AutocompleteEmpty>          <AutocompleteList>            {(site: string) => <AutocompleteItem key={site} value={site}>{site}</AutocompleteItem>}          </AutocompleteList>        </AutocompleteContent>      </Autocomplete>    </div>  )}

mode="both" filters the list and completes the input: the rest of the highlighted suggestion appears after the caret as selected text, so the next keystroke overwrites it rather than fighting it. Pair it with autoHighlight or there is nothing to complete from.

Use it where the vocabulary is closed and familiar — site names, scanner names. Avoid it in free prose, where a completion the writer did not ask for reads as the field arguing with them.

Suggestions that are not matches

'use client'import { SearchIcon } from 'lucide-react'import {  Autocomplete, AutocompleteClear, AutocompleteContent, AutocompleteGroup,  AutocompleteGroupLabel, AutocompleteInput, AutocompleteInputGroup,  AutocompleteItem, AutocompleteList,} from '@/registry/qure/ui/autocomplete'const recent = [  'ACC-482913',  'chest x-ray past SLA',  'unread, Apollo Chennai',  'Dr Mehta, last 7 days',]export default function AutocompleteSearch() {  return (    <div className="w-80">      {/*        `mode="none"` keeps the list static — these are recent searches, not        matches, so filtering them as the query is typed would make them        disappear the moment they became irrelevant to the new query rather        than to the reader. `openOnInputClick` shows them on focus.      */}      <Autocomplete items={recent} mode="none" openOnInputClick>        <AutocompleteInputGroup>          <SearchIcon className="size-4" />          <AutocompleteInput            size="sm"            type="search"            aria-label="Search the worklist"            placeholder="Search the worklist"          />          <AutocompleteClear />        </AutocompleteInputGroup>        <AutocompleteContent>          <AutocompleteList>            <AutocompleteGroup>              <AutocompleteGroupLabel>Recent</AutocompleteGroupLabel>              {recent.map(query => (                <AutocompleteItem key={query} value={query}>{query}</AutocompleteItem>              ))}            </AutocompleteGroup>          </AutocompleteList>        </AutocompleteContent>      </Autocomplete>    </div>  )}

mode="none" leaves the list static. These are recent searches, not results: filtering them against the new query would make them vanish exactly when they stop matching the half-typed thing, which is not the same as ceasing to be useful. openOnInputClick shows them on focus.

Groups

'use client'import {  Autocomplete, AutocompleteCollection, AutocompleteContent, AutocompleteEmpty,  AutocompleteGroup, AutocompleteGroupLabel, AutocompleteInput, AutocompleteItem,  AutocompleteList,} from '@/registry/qure/ui/autocomplete'import { Label } from '@/registry/qure/ui/label'const phrases = [  {    value: 'Normal',    items: [      'No acute cardiopulmonary abnormality.',      'Lungs are clear. No pleural effusion or pneumothorax.',    ],  },  {    value: 'Follow-up',    items: [      'Recommend follow-up CT in three months.',      'Comparison with prior study of the same date is advised.',    ],  },  {    value: 'Critical',    items: [      'Findings communicated to the referring clinician by telephone.',      'Large pneumothorax — immediate clinical correlation advised.',    ],  },]export default function AutocompleteGroups() {  return (    <div className="flex w-96 flex-col gap-2">      <Label htmlFor="phrase">Report phrase</Label>      <Autocomplete items={phrases}>        <AutocompleteInput id="phrase" placeholder="Start a sentence" />        <AutocompleteContent>          <AutocompleteEmpty>No stock phrase matches.</AutocompleteEmpty>          <AutocompleteList>            {(group: { value: string; items: string[] }) => (              <AutocompleteGroup key={group.value} items={group.items}>                <AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>                <AutocompleteCollection>                  {(phrase: string) => (                    <AutocompleteItem key={phrase} value={phrase}>{phrase}</AutocompleteItem>                  )}                </AutocompleteCollection>              </AutocompleteGroup>            )}          </AutocompleteList>        </AutocompleteContent>      </Autocomplete>    </div>  )}

Grouped items are { value, items }, the same shape as Combobox. Stock phrases group well because the reader is looking for a category first and a sentence second.

Modes

mode is the whole behaviour of the component in one prop.

ModeFilters the listCompletes the input
list (default)YesNo
bothYesYes
inlineNoYes
noneNoNo

Keyboard

Observed: typing opens and filters; ArrowDown moves into the list and loops back to the input past the end; Enter takes the highlighted suggestion into the input; Escape closes the list and leaves whatever was typed in place — it does not revert the field, which is the point of the control.

API Reference

Everything Base UI's Autocomplete accepts. What we add on AutocompleteInput:

Prop

Type

On AutocompleteContent:

Prop

Type

On Autocomplete itself:

Prop

Type

On this page