Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Autocomplete

An input that suggests options as you type.

Rendered by React 19.3.1

Loading React…
import { Autocomplete } from '@hamur/react/autocomplete'

const tags = ['feature', 'fix', 'bug', 'docs', 'internal', 'mobile', 'performance', 'release']

export default function AutocompleteExample() {
  return (
    <Autocomplete.Root items={tags}>
      <label className="field">
        <span className="select-label">Search tags</span>
        <Autocomplete.Input className="input" placeholder="e.g. feature" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner
          className="select-positioner"
          sideOffset={6}
        >
          <Autocomplete.Popup className="select-popup combobox-popup">
            <Autocomplete.Empty className="combobox-empty">
              No tags found.
            </Autocomplete.Empty>
            <Autocomplete.List className="select-list">
              <Autocomplete.Collection>
                {(tag: string) => (
                  <Autocomplete.Item
                    key={tag}
                    value={tag}
                    className="select-item plain"
                  >
                    {tag}
                  </Autocomplete.Item>
                )}
              </Autocomplete.Collection>
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}
224Tests passing in React · 0 failing
61.7 KiBSize
5.00 msOpen
18.1 msClose
43.0 msOpenMutations
43.0 msCloseMutations
1.00 MiBMemory
productdevbook and 18 upstream authors5 commits · last

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Autocomplete.Root>
  <Autocomplete.Input />
  <Autocomplete.Portal>
    <Autocomplete.Positioner>
      <Autocomplete.Popup>
        <Autocomplete.Empty />
        <Autocomplete.List>
          <Autocomplete.Collection>
            <Autocomplete.Item />
          </Autocomplete.Collection>
        </Autocomplete.List>
      </Autocomplete.Popup>
    </Autocomplete.Positioner>
  </Autocomplete.Portal>
</Autocomplete.Root>

// Not in this example: Autocomplete.Arrow, Autocomplete.Backdrop, Autocomplete.Clear, Autocomplete.Group, Autocomplete.GroupLabel, Autocomplete.Icon, Autocomplete.InputGroup, Autocomplete.Row, Autocomplete.Separator, Autocomplete.Status, Autocomplete.Trigger, Autocomplete.Value

Examples

Async

Items load while typing; Status shows the loading text.

AutocompleteAsync.tsx
import * as React from 'react'
import { Autocomplete } from '@hamur/react/autocomplete'

interface Movie {
  id: string
  title: string
  year: number
}

export default function AutocompleteAsyncExample() {
  const [searchValue, setSearchValue] = React.useState('')
  const [searchResults, setSearchResults] = React.useState<Movie[]>([])
  const [error, setError] = React.useState<string | null>(null)

  const [isPending, startTransition] = React.useTransition()

  const { contains } = Autocomplete.useFilter()

  const abortControllerRef = React.useRef<AbortController | null>(null)

  function getStatus(): React.ReactNode | null {
    if (isPending) {
      return (
        <React.Fragment>
          <span className="combobox-spinner" aria-hidden />
          Searching…
        </React.Fragment>
      )
    }
    if (error) {
      return error
    }
    if (searchValue === '') {
      return null
    }
    if (searchResults.length === 0) {
      return `Movie or year "${searchValue}" does not exist in the Top 100 IMDb movies`
    }
    return `${searchResults.length} result${searchResults.length === 1 ? '' : 's'} found`
  }

  const status = getStatus()

  return (
    <Autocomplete.Root
      items={searchResults}
      value={searchValue}
      onValueChange={(nextSearchValue) => {
        setSearchValue(nextSearchValue)

        const controller = new AbortController()
        abortControllerRef.current?.abort()
        abortControllerRef.current = controller

        if (nextSearchValue === '') {
          setSearchResults([])
          setError(null)
          return
        }

        startTransition(async () => {
          setError(null)
          const result = await searchMovies(nextSearchValue, contains)
          if (controller.signal.aborted) {
            return
          }
          startTransition(() => {
            setSearchResults(result.movies)
            setError(result.error)
          })
        })
      }}
      itemToStringValue={(item: Movie) => item.title}
      filter={null}
    >
      <label className="field">
        <span className="select-label">Search movies by name or year</span>
        <Autocomplete.Input
          className="input"
          placeholder="e.g. Pulp Fiction or 1994"
        />
      </label>
      <Autocomplete.Portal hidden={!status}>
        <Autocomplete.Positioner
          className="select-positioner"
          sideOffset={4}
          align="start"
        >
          <Autocomplete.Popup
            className="select-popup combobox-popup"
            aria-busy={isPending || undefined}
          >
            <Autocomplete.Status>
              {status && <div className="combobox-status">{status}</div>}
            </Autocomplete.Status>
            <Autocomplete.List className="select-list">
              <Autocomplete.Collection>
                {(movie: Movie) => (
                  <Autocomplete.Item
                    key={movie.id}
                    value={movie}
                    className="select-item plain"
                  >
                    <span className="autocomplete-movie">
                      <span>{movie.title}</span>
                      <span className="combobox-person-detail">{movie.year}</span>
                    </span>
                  </Autocomplete.Item>
                )}
              </Autocomplete.Collection>
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

async function searchMovies(
  query: string,
  filter: (item: string, query: string) => boolean,
): Promise<{ movies: Movie[]; error: string | null }> {
  // A pretend network delay.
  await new Promise((resolve) => {
    setTimeout(resolve, Math.random() * 500 + 100)
  })
  // A pretend network error, 1% of the time or for this query.
  if (Math.random() < 0.01 || query === 'will_error') {
    return { movies: [], error: 'Failed to fetch movies. Please try again.' }
  }
  const movies = top100Movies.filter(
    (movie) => filter(movie.title, query) || filter(movie.year.toString(), query),
  )
  return { movies, error: null }
}

const top100Movies: Movie[] = [
  { id: '1', title: 'The Shawshank Redemption', year: 1994 },
  { id: '2', title: 'The Godfather', year: 1972 },
  { id: '3', title: 'The Dark Knight', year: 2008 },
  { id: '4', title: 'The Godfather Part II', year: 1974 },
  { id: '5', title: '12 Angry Men', year: 1957 },
  { id: '6', title: 'The Lord of the Rings: The Return of the King', year: 2003 },
  { id: '7', title: "Schindler's List", year: 1993 },
  { id: '8', title: 'Pulp Fiction', year: 1994 },
  { id: '9', title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 },
  { id: '10', title: 'The Good, the Bad and the Ugly', year: 1966 },
  { id: '11', title: 'Forrest Gump', year: 1994 },
  { id: '12', title: 'The Lord of the Rings: The Two Towers', year: 2002 },
  { id: '13', title: 'Fight Club', year: 1999 },
  { id: '14', title: 'Inception', year: 2010 },
  { id: '15', title: 'Star Wars: Episode V – The Empire Strikes Back', year: 1980 },
  { id: '16', title: 'The Matrix', year: 1999 },
  { id: '17', title: 'Goodfellas', year: 1990 },
  { id: '18', title: 'Interstellar', year: 2014 },
  { id: '19', title: "One Flew Over the Cuckoo's Nest", year: 1975 },
  { id: '20', title: 'Se7en', year: 1995 },
  { id: '21', title: "It's a Wonderful Life", year: 1946 },
  { id: '22', title: 'The Silence of the Lambs', year: 1991 },
  { id: '23', title: 'Seven Samurai', year: 1954 },
  { id: '24', title: 'Saving Private Ryan', year: 1998 },
  { id: '25', title: 'City of God', year: 2002 },
  { id: '26', title: 'Life Is Beautiful', year: 1997 },
  { id: '27', title: 'The Green Mile', year: 1999 },
  { id: '28', title: 'Star Wars: Episode IV – A New Hope', year: 1977 },
  { id: '29', title: 'Terminator 2: Judgment Day', year: 1991 },
  { id: '30', title: 'Back to the Future', year: 1985 },
  { id: '31', title: 'Spirited Away', year: 2001 },
  { id: '32', title: 'The Pianist', year: 2002 },
  { id: '33', title: 'Psycho', year: 1960 },
  { id: '34', title: 'Parasite', year: 2019 },
  { id: '35', title: 'Gladiator', year: 2000 },
  { id: '36', title: 'Léon: The Professional', year: 1994 },
  { id: '37', title: 'American History X', year: 1998 },
  { id: '38', title: 'The Departed', year: 2006 },
  { id: '39', title: 'Whiplash', year: 2014 },
  { id: '40', title: 'The Prestige', year: 2006 },
  { id: '41', title: 'Grave of the Fireflies', year: 1988 },
  { id: '42', title: 'The Usual Suspects', year: 1995 },
  { id: '43', title: 'Casablanca', year: 1942 },
  { id: '44', title: 'Harakiri', year: 1962 },
  { id: '45', title: 'The Lion King', year: 1994 },
  { id: '46', title: 'The Intouchables', year: 2011 },
  { id: '47', title: 'Modern Times', year: 1936 },
  { id: '48', title: 'The Lives of Others', year: 2006 },
  { id: '49', title: 'Once Upon a Time in the West', year: 1968 },
  { id: '50', title: 'Rear Window', year: 1954 },
  { id: '51', title: 'Alien', year: 1979 },
  { id: '52', title: 'City Lights', year: 1931 },
  { id: '53', title: 'The Shining', year: 1980 },
  { id: '54', title: 'Cinema Paradiso', year: 1988 },
  { id: '55', title: 'Avengers: Infinity War', year: 2018 },
  { id: '56', title: 'Paths of Glory', year: 1957 },
  { id: '57', title: 'Django Unchained', year: 2012 },
  { id: '58', title: 'WALL·E', year: 2008 },
  { id: '59', title: 'Sunset Boulevard', year: 1950 },
  { id: '60', title: 'The Great Dictator', year: 1940 },
  { id: '61', title: 'The Dark Knight Rises', year: 2012 },
  { id: '62', title: 'Princess Mononoke', year: 1997 },
  { id: '63', title: 'Witness for the Prosecution', year: 1957 },
  { id: '64', title: 'Oldboy', year: 2003 },
  { id: '65', title: 'Aliens', year: 1986 },
  { id: '66', title: 'Once Upon a Time in America', year: 1984 },
  { id: '67', title: 'Coco', year: 2017 },
  { id: '68', title: 'Your Name.', year: 2016 },
  { id: '69', title: 'American Beauty', year: 1999 },
  { id: '70', title: 'Braveheart', year: 1995 },
  { id: '71', title: 'Das Boot', year: 1981 },
  { id: '72', title: '3 Idiots', year: 2009 },
  { id: '73', title: 'Toy Story', year: 1995 },
  { id: '74', title: 'Inglourious Basterds', year: 2009 },
  { id: '75', title: 'High and Low', year: 1963 },
  { id: '76', title: 'Amadeus', year: 1984 },
  { id: '77', title: 'Good Will Hunting', year: 1997 },
  { id: '78', title: 'Star Wars: Episode VI – Return of the Jedi', year: 1983 },
  { id: '79', title: 'The Hunt', year: 2012 },
  { id: '80', title: 'Capharnaüm', year: 2018 },
  { id: '81', title: 'Reservoir Dogs', year: 1992 },
  { id: '82', title: 'Eternal Sunshine of the Spotless Mind', year: 2004 },
  { id: '83', title: 'Requiem for a Dream', year: 2000 },
  { id: '84', title: 'Come and See', year: 1985 },
  { id: '85', title: 'Ikiru', year: 1952 },
  { id: '86', title: 'Vertigo', year: 1958 },
  { id: '87', title: 'Lawrence of Arabia', year: 1962 },
  { id: '88', title: 'Citizen Kane', year: 1941 },
  { id: '89', title: 'Memento', year: 2000 },
  { id: '90', title: 'North by Northwest', year: 1959 },
  { id: '91', title: 'Star Wars: Episode III – Revenge of the Sith', year: 2005 },
  { id: '92', title: '2001: A Space Odyssey', year: 1968 },
  { id: '93', title: 'Amélie', year: 2001 },
  { id: '94', title: "Singin' in the Rain", year: 1952 },
  { id: '95', title: 'Apocalypse Now', year: 1979 },
  { id: '96', title: 'Taxi Driver', year: 1976 },
  { id: '97', title: 'Downfall', year: 2004 },
  { id: '98', title: 'The Wolf of Wall Street', year: 2013 },
  { id: '99', title: 'A Clockwork Orange', year: 1971 },
  { id: '100', title: 'Double Indemnity', year: 1944 },
]

Inline

mode fills the input with the highlighted item while arrowing through the list.

AutocompleteInline.tsx
import { Autocomplete } from '@hamur/react/autocomplete'

interface Tag {
  id: string
  value: string
}

const tags: Tag[] = [
  { id: 't1', value: 'feature' },
  { id: 't2', value: 'fix' },
  { id: 't3', value: 'bug' },
  { id: 't4', value: 'docs' },
  { id: 't5', value: 'internal' },
  { id: 't6', value: 'mobile' },
  { id: 'c-accordion', value: 'component: accordion' },
  { id: 'c-alert-dialog', value: 'component: alert dialog' },
  { id: 'c-autocomplete', value: 'component: autocomplete' },
  { id: 'c-avatar', value: 'component: avatar' },
  { id: 'c-checkbox', value: 'component: checkbox' },
  { id: 'c-checkbox-group', value: 'component: checkbox group' },
  { id: 'c-collapsible', value: 'component: collapsible' },
  { id: 'c-combobox', value: 'component: combobox' },
  { id: 'c-context-menu', value: 'component: context menu' },
  { id: 'c-dialog', value: 'component: dialog' },
  { id: 'c-field', value: 'component: field' },
  { id: 'c-fieldset', value: 'component: fieldset' },
  { id: 'c-filterable-menu', value: 'component: filterable menu' },
  { id: 'c-form', value: 'component: form' },
  { id: 'c-input', value: 'component: input' },
  { id: 'c-menu', value: 'component: menu' },
  { id: 'c-menubar', value: 'component: menubar' },
  { id: 'c-meter', value: 'component: meter' },
  { id: 'c-navigation-menu', value: 'component: navigation menu' },
  { id: 'c-number-field', value: 'component: number field' },
  { id: 'c-popover', value: 'component: popover' },
  { id: 'c-preview-card', value: 'component: preview card' },
  { id: 'c-progress', value: 'component: progress' },
  { id: 'c-radio', value: 'component: radio' },
  { id: 'c-scroll-area', value: 'component: scroll area' },
  { id: 'c-select', value: 'component: select' },
  { id: 'c-separator', value: 'component: separator' },
  { id: 'c-slider', value: 'component: slider' },
  { id: 'c-switch', value: 'component: switch' },
  { id: 'c-tabs', value: 'component: tabs' },
  { id: 'c-toast', value: 'component: toast' },
  { id: 'c-toggle', value: 'component: toggle' },
  { id: 'c-toggle-group', value: 'component: toggle group' },
  { id: 'c-toolbar', value: 'component: toolbar' },
  { id: 'c-tooltip', value: 'component: tooltip' },
]

export default function AutocompleteInlineExample() {
  return (
    <Autocomplete.Root items={tags} mode="both">
      <label className="field">
        <span className="select-label">Search tags</span>
        <Autocomplete.Input className="input" placeholder="e.g. feature" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner
          className="select-positioner"
          sideOffset={4}
        >
          <Autocomplete.Popup className="select-popup combobox-popup">
            <Autocomplete.List className="select-list">
              <Autocomplete.Collection>
                {(tag: Tag) => (
                  <Autocomplete.Item
                    key={tag.id}
                    value={tag}
                    className="select-item plain"
                  >
                    {tag.value}
                  </Autocomplete.Item>
                )}
              </Autocomplete.Collection>
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Grouped

Group and GroupLabel add headings inside the popup.

AutocompleteGrouped.tsx
import { Autocomplete } from '@hamur/react/autocomplete'

export default function AutocompleteGroupedExample() {
  return (
    <Autocomplete.Root items={groupedTags}>
      <label className="field">
        <span className="select-label">Select a tag</span>
        <Autocomplete.Input className="input" placeholder="e.g. feature" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner
          className="select-positioner"
          sideOffset={4}
        >
          <Autocomplete.Popup className="select-popup combobox-popup">
            <Autocomplete.Empty>
              <div className="combobox-empty">No tags found.</div>
            </Autocomplete.Empty>
            <Autocomplete.List className="select-list">
              <Autocomplete.Collection>
                {(group: TagGroup) => (
                  <Autocomplete.Group
                    key={group.value}
                    items={group.items}
                    className="select-group"
                  >
                    <Autocomplete.GroupLabel className="select-group-label">
                      {group.value}
                    </Autocomplete.GroupLabel>
                    <Autocomplete.Collection>
                      {(tag: Tag) => (
                        <Autocomplete.Item
                          key={tag.id}
                          value={tag}
                          className="select-item plain"
                        >
                          {tag.label}
                        </Autocomplete.Item>
                      )}
                    </Autocomplete.Collection>
                  </Autocomplete.Group>
                )}
              </Autocomplete.Collection>
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

interface Tag {
  id: string
  label: string
  group: 'Type' | 'Component'
}

interface TagGroup {
  value: string
  items: Tag[]
}

const tagsData: Tag[] = [
  { id: 't1', label: 'feature', group: 'Type' },
  { id: 't2', label: 'fix', group: 'Type' },
  { id: 't3', label: 'bug', group: 'Type' },
  { id: 't4', label: 'docs', group: 'Type' },
  { id: 't5', label: 'internal', group: 'Type' },
  { id: 't6', label: 'mobile', group: 'Type' },
  { id: 'c-accordion', label: 'component: accordion', group: 'Component' },
  { id: 'c-alert-dialog', label: 'component: alert dialog', group: 'Component' },
  { id: 'c-autocomplete', label: 'component: autocomplete', group: 'Component' },
  { id: 'c-avatar', label: 'component: avatar', group: 'Component' },
  { id: 'c-checkbox', label: 'component: checkbox', group: 'Component' },
  { id: 'c-checkbox-group', label: 'component: checkbox group', group: 'Component' },
  { id: 'c-collapsible', label: 'component: collapsible', group: 'Component' },
  { id: 'c-combobox', label: 'component: combobox', group: 'Component' },
  { id: 'c-context-menu', label: 'component: context menu', group: 'Component' },
  { id: 'c-dialog', label: 'component: dialog', group: 'Component' },
  { id: 'c-field', label: 'component: field', group: 'Component' },
  { id: 'c-fieldset', label: 'component: fieldset', group: 'Component' },
  { id: 'c-filterable-menu', label: 'component: filterable menu', group: 'Component' },
  { id: 'c-form', label: 'component: form', group: 'Component' },
  { id: 'c-input', label: 'component: input', group: 'Component' },
  { id: 'c-menu', label: 'component: menu', group: 'Component' },
  { id: 'c-menubar', label: 'component: menubar', group: 'Component' },
  { id: 'c-meter', label: 'component: meter', group: 'Component' },
  { id: 'c-navigation-menu', label: 'component: navigation menu', group: 'Component' },
  { id: 'c-number-field', label: 'component: number field', group: 'Component' },
  { id: 'c-popover', label: 'component: popover', group: 'Component' },
  { id: 'c-preview-card', label: 'component: preview card', group: 'Component' },
  { id: 'c-progress', label: 'component: progress', group: 'Component' },
  { id: 'c-radio', label: 'component: radio', group: 'Component' },
  { id: 'c-scroll-area', label: 'component: scroll area', group: 'Component' },
  { id: 'c-select', label: 'component: select', group: 'Component' },
  { id: 'c-separator', label: 'component: separator', group: 'Component' },
  { id: 'c-slider', label: 'component: slider', group: 'Component' },
  { id: 'c-switch', label: 'component: switch', group: 'Component' },
  { id: 'c-tabs', label: 'component: tabs', group: 'Component' },
  { id: 'c-toast', label: 'component: toast', group: 'Component' },
  { id: 'c-toggle', label: 'component: toggle', group: 'Component' },
  { id: 'c-toggle-group', label: 'component: toggle group', group: 'Component' },
  { id: 'c-toolbar', label: 'component: toolbar', group: 'Component' },
  { id: 'c-tooltip', label: 'component: tooltip', group: 'Component' },
]

function groupTags(tags: Tag[]): TagGroup[] {
  const groups: { [key: string]: Tag[] } = {}
  tags.forEach((t) => {
    ;(groups[t.group] ??= []).push(t)
  })
  const order = ['Type', 'Component']
  return order.map((value) => ({ value, items: groups[value] ?? [] }))
}

const groupedTags: TagGroup[] = groupTags(tagsData)

Fuzzy Matching

A custom filter matches items that are close to the query.

AutocompleteFuzzyMatching.tsx
import { Autocomplete } from '@hamur/react/autocomplete'

interface FuzzyItem {
  title: string
  description: string
  category: string
}

export default function AutocompleteFuzzyMatchingExample() {
  return (
    <Autocomplete.Root
      items={fuzzyItems}
      filter={fuzzyFilter}
      itemToStringValue={(item: FuzzyItem) => item.title}
    >
      <label className="field">
        <span className="select-label">Fuzzy search documentation</span>
        <Autocomplete.Input className="input" placeholder="e.g. React" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner
          className="select-positioner"
          sideOffset={4}
        >
          <Autocomplete.Popup className="select-popup combobox-popup">
            <Autocomplete.Empty>
              <div className="combobox-empty">
                No results found for "<Autocomplete.Value />"
              </div>
            </Autocomplete.Empty>
            <Autocomplete.List className="select-list">
              <Autocomplete.Collection>
                {(item: FuzzyItem) => (
                  <Autocomplete.Item
                    key={item.title}
                    value={item}
                    className="select-item plain"
                  >
                    <Autocomplete.Value>
                      {(value: string) => (
                        <span className="combobox-person autocomplete-match">
                          <span>{highlight(item.title, value)}</span>
                          <span className="combobox-person-detail">
                            <span>{highlight(item.description, value)}</span>
                          </span>
                        </span>
                      )}
                    </Autocomplete.Value>
                  </Autocomplete.Item>
                )}
              </Autocomplete.Collection>
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

function highlight(text: string, query: string) {
  return highlightParts(text, query).map((part, index) =>
    part.mark ? <mark key={index}>{part.text}</mark> : part.text,
  )
}

// Upstream ranks with match-sorter; this matches the same way without it: an item matches when
// the letters of the query appear in order in its title, description or category.
function fuzzyMatch(text: string, query: string): boolean {
  const haystack = text.toLowerCase()
  let at = 0
  for (const char of query.toLowerCase()) {
    at = haystack.indexOf(char, at)
    if (at === -1) return false
    at += 1
  }
  return true
}

function fuzzyFilter(item: FuzzyItem, query: string): boolean {
  if (!query) {
    return true
  }
  return [item.title, item.description, item.category].some((text) => fuzzyMatch(text, query))
}

// The text split around each run of the query, the matches marked.
function highlightParts(text: string, query: string): { text: string; mark: boolean }[] {
  const trimmed = query.trim()
  if (!trimmed) {
    return [{ text, mark: false }]
  }
  const limited = trimmed.slice(0, 100)
  const escaped = limited.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  const regex = new RegExp(`(${escaped})`, 'gi')
  return text
    .split(regex)
    .filter((part) => part !== '')
    .map((part) => ({ text: part, mark: part.toLowerCase() === limited.toLowerCase() }))
}

const fuzzyItems: FuzzyItem[] = [
  {
    title: 'React Hooks Guide',
    description: 'Learn how to use React Hooks like useState, useEffect, and custom hooks',
    category: 'React',
  },
  {
    title: 'JavaScript Array Methods',
    description: 'Master array methods like map, filter, reduce, and forEach in JavaScript',
    category: 'JavaScript',
  },
  {
    title: 'CSS Flexbox Layout',
    description: 'Complete guide to CSS Flexbox for responsive web design',
    category: 'CSS',
  },
  {
    title: 'TypeScript Interfaces',
    description: 'Understanding TypeScript interfaces and type definitions',
    category: 'TypeScript',
  },
  {
    title: 'React Performance Optimization',
    description: 'Tips and techniques for optimizing React application performance',
    category: 'React',
  },
  {
    title: 'HTML Semantic Elements',
    description: 'Using semantic HTML elements for better accessibility and SEO',
    category: 'HTML',
  },
  {
    title: 'Node.js Express Server',
    description: 'Building RESTful APIs with Node.js and Express framework',
    category: 'Node.js',
  },
  {
    title: 'Vue Composition API',
    description: 'Modern Vue.js development using the Composition API',
    category: 'Vue.js',
  },
  {
    title: 'Angular Components',
    description: 'Creating reusable Angular components with TypeScript',
    category: 'Angular',
  },
  {
    title: 'Python Django Framework',
    description: 'Web development with Python Django framework',
    category: 'Python',
  },
  {
    title: 'CSS Grid Layout',
    description: 'Advanced CSS Grid techniques for complex layouts',
    category: 'CSS',
  },
  {
    title: 'React Testing Library',
    description: 'Testing React components with React Testing Library',
    category: 'React',
  },
  {
    title: 'MongoDB Queries',
    description: 'Advanced MongoDB queries and aggregation pipelines',
    category: 'Database',
  },
  {
    title: 'Webpack Configuration',
    description: 'Optimizing webpack configuration for production builds',
    category: 'Build Tools',
  },
  {
    title: 'SASS/SCSS Guide',
    description: 'Writing maintainable CSS with SASS and SCSS',
    category: 'CSS',
  },
]

Limit

limit caps the visible items and Status asks for a narrower query.

AutocompleteLimit.tsx
import * as React from 'react'
import { Autocomplete } from '@hamur/react/autocomplete'

interface Tag {
  id: string
  value: string
}

const limit = 8

const tags: Tag[] = [
  { id: 't1', value: 'feature' },
  { id: 't2', value: 'fix' },
  { id: 't3', value: 'bug' },
  { id: 't4', value: 'docs' },
  { id: 't5', value: 'internal' },
  { id: 't6', value: 'mobile' },
  { id: 't7', value: 'frontend' },
  { id: 't8', value: 'backend' },
  { id: 't9', value: 'performance' },
  { id: 't10', value: 'accessibility' },
  { id: 't11', value: 'design' },
  { id: 't12', value: 'research' },
  { id: 't13', value: 'testing' },
  { id: 't14', value: 'infrastructure' },
  { id: 't15', value: 'documentation' },
  { id: 'c-accordion', value: 'component: accordion' },
  { id: 'c-alert-dialog', value: 'component: alert dialog' },
  { id: 'c-autocomplete', value: 'component: autocomplete' },
  { id: 'c-avatar', value: 'component: avatar' },
  { id: 'c-checkbox', value: 'component: checkbox' },
  { id: 'c-checkbox-group', value: 'component: checkbox group' },
  { id: 'c-collapsible', value: 'component: collapsible' },
  { id: 'c-combobox', value: 'component: combobox' },
  { id: 'c-context-menu', value: 'component: context menu' },
  { id: 'c-dialog', value: 'component: dialog' },
  { id: 'c-field', value: 'component: field' },
  { id: 'c-fieldset', value: 'component: fieldset' },
  { id: 'c-filterable-menu', value: 'component: filterable menu' },
  { id: 'c-form', value: 'component: form' },
  { id: 'c-input', value: 'component: input' },
  { id: 'c-menu', value: 'component: menu' },
  { id: 'c-menubar', value: 'component: menubar' },
  { id: 'c-meter', value: 'component: meter' },
  { id: 'c-navigation-menu', value: 'component: navigation menu' },
  { id: 'c-number-field', value: 'component: number field' },
  { id: 'c-popover', value: 'component: popover' },
  { id: 'c-preview-card', value: 'component: preview card' },
  { id: 'c-progress', value: 'component: progress' },
  { id: 'c-radio', value: 'component: radio' },
  { id: 'c-scroll-area', value: 'component: scroll area' },
  { id: 'c-select', value: 'component: select' },
  { id: 'c-separator', value: 'component: separator' },
  { id: 'c-slider', value: 'component: slider' },
  { id: 'c-switch', value: 'component: switch' },
  { id: 'c-tabs', value: 'component: tabs' },
  { id: 'c-toast', value: 'component: toast' },
  { id: 'c-toggle', value: 'component: toggle' },
  { id: 'c-toggle-group', value: 'component: toggle group' },
  { id: 'c-toolbar', value: 'component: toolbar' },
  { id: 'c-tooltip', value: 'component: tooltip' },
]

export default function AutocompleteLimitExample() {
  const [value, setValue] = React.useState('')

  const { contains } = Autocomplete.useFilter({ sensitivity: 'base' })

  const totalMatches = React.useMemo(() => {
    const trimmed = value.trim()
    if (!trimmed) {
      return tags.length
    }
    return tags.filter((t) => contains(t.value, trimmed)).length
  }, [value, contains])

  const moreCount = Math.max(0, totalMatches - limit)

  return (
    <Autocomplete.Root
      items={tags}
      value={value}
      onValueChange={setValue}
      limit={limit}
    >
      <label className="field">
        <span className="select-label">Limit results to 8</span>
        <Autocomplete.Input className="input" placeholder="e.g. component" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner
          className="select-positioner"
          sideOffset={4}
        >
          <Autocomplete.Popup className="select-popup combobox-popup">
            <Autocomplete.Empty>
              <div className="combobox-empty">No results found for "{value}"</div>
            </Autocomplete.Empty>
            <Autocomplete.List className="select-list">
              <Autocomplete.Collection>
                {(tag: Tag) => (
                  <Autocomplete.Item
                    key={tag.id}
                    value={tag}
                    className="select-item plain"
                  >
                    {tag.value}
                  </Autocomplete.Item>
                )}
              </Autocomplete.Collection>
            </Autocomplete.List>
            <Autocomplete.Status>
              {moreCount > 0 ? (
                <div className="combobox-status">
                  {`Hiding ${moreCount} results (type a more specific query to narrow results)`}
                </div>
              ) : null}
            </Autocomplete.Status>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Auto Highlight

autoHighlight highlights the first match as the person types.

AutocompleteAutoHighlight.tsx
import { Autocomplete } from '@hamur/react/autocomplete'

interface Tag {
  id: string
  value: string
}

const tags: Tag[] = [
  { id: 't1', value: 'feature' },
  { id: 't2', value: 'fix' },
  { id: 't3', value: 'bug' },
  { id: 't4', value: 'docs' },
  { id: 't5', value: 'internal' },
  { id: 't6', value: 'mobile' },
  { id: 'c-accordion', value: 'component: accordion' },
  { id: 'c-alert-dialog', value: 'component: alert dialog' },
  { id: 'c-autocomplete', value: 'component: autocomplete' },
  { id: 'c-avatar', value: 'component: avatar' },
  { id: 'c-checkbox', value: 'component: checkbox' },
  { id: 'c-checkbox-group', value: 'component: checkbox group' },
  { id: 'c-collapsible', value: 'component: collapsible' },
  { id: 'c-combobox', value: 'component: combobox' },
  { id: 'c-context-menu', value: 'component: context menu' },
  { id: 'c-dialog', value: 'component: dialog' },
  { id: 'c-field', value: 'component: field' },
  { id: 'c-fieldset', value: 'component: fieldset' },
  { id: 'c-filterable-menu', value: 'component: filterable menu' },
  { id: 'c-form', value: 'component: form' },
  { id: 'c-input', value: 'component: input' },
  { id: 'c-menu', value: 'component: menu' },
  { id: 'c-menubar', value: 'component: menubar' },
  { id: 'c-meter', value: 'component: meter' },
  { id: 'c-navigation-menu', value: 'component: navigation menu' },
  { id: 'c-number-field', value: 'component: number field' },
  { id: 'c-popover', value: 'component: popover' },
  { id: 'c-preview-card', value: 'component: preview card' },
  { id: 'c-progress', value: 'component: progress' },
  { id: 'c-radio', value: 'component: radio' },
  { id: 'c-scroll-area', value: 'component: scroll area' },
  { id: 'c-select', value: 'component: select' },
  { id: 'c-separator', value: 'component: separator' },
  { id: 'c-slider', value: 'component: slider' },
  { id: 'c-switch', value: 'component: switch' },
  { id: 'c-tabs', value: 'component: tabs' },
  { id: 'c-toast', value: 'component: toast' },
  { id: 'c-toggle', value: 'component: toggle' },
  { id: 'c-toggle-group', value: 'component: toggle group' },
  { id: 'c-toolbar', value: 'component: toolbar' },
  { id: 'c-tooltip', value: 'component: tooltip' },
]

export default function AutocompleteAutoHighlightExample() {
  return (
    <Autocomplete.Root items={tags} autoHighlight>
      <label className="field">
        <span className="select-label">Auto highlight on type</span>
        <Autocomplete.Input className="input" placeholder="e.g. feature" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner
          className="select-positioner"
          sideOffset={4}
        >
          <Autocomplete.Popup className="select-popup combobox-popup">
            <Autocomplete.Empty>
              <div className="combobox-empty">No tags found.</div>
            </Autocomplete.Empty>
            <Autocomplete.List className="select-list">
              <Autocomplete.Collection>
                {(tag: Tag) => (
                  <Autocomplete.Item
                    key={tag.id}
                    value={tag}
                    className="select-item plain"
                  >
                    {tag.value}
                  </Autocomplete.Item>
                )}
              </Autocomplete.Collection>
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Virtualized

A virtualization library renders only the visible items of a long list.

AutocompleteVirtualized.tsx
import * as React from 'react'
import { Autocomplete } from '@hamur/react/autocomplete'

interface VirtualizedItem {
  id: string
  name: string
}

// Upstream windows the list with @tanstack/react-virtual; this one does the same by hand with
// rows of a fixed height.
const ROW = 32
const PAD = 4
const VIEWPORT = 260
const OVERSCAN = 20

interface Scroller {
  scrollToIndex: (index: number, align: 'start' | 'end') => void
  count: number
}

export default function AutocompleteVirtualizedExample() {
  const scrollerRef = React.useRef<Scroller | null>(null)

  return (
    <Autocomplete.Root
      virtualized
      openOnInputClick
      items={virtualizedItems}
      itemToStringValue={getItemLabel}
      onItemHighlighted={(item, { reason, index }) => {
        const scroller = scrollerRef.current
        if (!item || !scroller) {
          return
        }
        const isStart = index === 0
        const isEnd = index === scroller.count - 1
        const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd))
        if (shouldScroll) {
          queueMicrotask(() => {
            scroller.scrollToIndex(index, isEnd ? 'start' : 'end')
          })
        }
      }}
    >
      <label className="field">
        <span className="select-label">Search 10,000 items</span>
        <Autocomplete.Input className="input" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner
          className="select-positioner"
          sideOffset={4}
        >
          <Autocomplete.Popup className="select-popup combobox-popup">
            <Autocomplete.Empty>
              <div className="combobox-empty">No items found.</div>
            </Autocomplete.Empty>
            <Autocomplete.List>
              <VirtualizedList scrollerRef={scrollerRef} />
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

function VirtualizedList({ scrollerRef }: { scrollerRef: React.RefObject<Scroller | null> }) {
  const filteredItems = Autocomplete.useFilteredItems<VirtualizedItem>()
  const elementRef = React.useRef<HTMLDivElement | null>(null)
  const [scrollTop, setScrollTop] = React.useState(0)

  const count = filteredItems.length
  const totalSize = count * ROW + PAD * 2

  React.useImperativeHandle(
    scrollerRef,
    () => ({
      count,
      scrollToIndex(index, align) {
        const element = elementRef.current
        if (!element) return
        const start = PAD + index * ROW
        element.scrollTop =
          align === 'start' ? start - PAD : start + ROW + PAD - element.clientHeight
      },
    }),
    [count],
  )

  if (!count) {
    return null
  }

  const first = Math.max(0, Math.floor((scrollTop - PAD) / ROW) - OVERSCAN)
  const last = Math.min(count, Math.ceil((scrollTop + VIEWPORT) / ROW) + OVERSCAN)
  const rows: number[] = []
  for (let index = first; index < last; index += 1) {
    rows.push(index)
  }

  return (
    <div
      role="presentation"
      ref={elementRef}
      className="select-list combobox-scroller"
      onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
    >
      <div role="presentation" className="combobox-virtual" style={{ height: totalSize }}>
        {rows.map((index) => {
          const item = filteredItems[index]
          return (
            <Autocomplete.Item
              key={item.id}
              index={index}
              data-index={index}
              value={item}
              className="select-item plain"
              aria-setsize={count}
              aria-posinset={index + 1}
              style={{ top: 0, transform: `translateY(${PAD + index * ROW}px)` }}
            >
              {item.name}
            </Autocomplete.Item>
          )
        })}
      </div>
    </div>
  )
}

function getItemLabel(item: VirtualizedItem | null) {
  return item ? item.name : ''
}

const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
  const id = String(index + 1)
  const indexLabel = id.padStart(4, '0')
  return { id, name: `Item ${indexLabel}` }
})

API reference

68 props across 21 parts, in React syntax. className, style, render are listed once, under Shared props.

ArrowAutocomplete.Arrow

Displays an element positioned against the anchor. Renders a <div>.

Arrow data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
data-uncenteredPresent when the arrow is uncentered.

BackdropAutocomplete.Backdrop

An overlay displayed beneath the popup. Renders a <div>.

Backdrop data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-starting-stylePresent when the popup begins animating in.
data-ending-stylePresent when the popup is animating out.

ClearAutocomplete.Clear

Clears the value when clicked. Renders a <button>.

Clear props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
disabledWhether the component should ignore user interaction.booleanfalse
keepMountedWhether the component should remain mounted in the DOM when not visible.booleanfalse
Clear data attributes
Data attributeDescription
data-popup-openPresent when the corresponding popup is open.
data-disabledPresent when the button is disabled.
data-visiblePresent when the clear button is visible.
data-starting-stylePresent when the button begins animating in.
data-ending-stylePresent when the button is animating out.

CollectionAutocomplete.Collection

Renders filtered list items. If rendering a flat list, pass a function child to the List component instead, which implicitly wraps it. Doesn’t render its own element.

Collection props
PropTypeDefault
childrenrequired(item: any, index: number) => React.ReactNode—

EmptyAutocomplete.Empty

Renders its children only when the list is empty. Requires the items prop on the root component. Announces changes politely to screen readers. This component's root element must remain mounted in the DOM to announce changes consistently across screen readers. Avoid hiding or removing the component itself with display: none, hidden, aria-hidden, or conditional rendering. Prefer updating or conditionally rendering its children instead. Renders a <div>.

GroupAutocomplete.Group

Groups related items with the corresponding label. Renders a <div>.

Group props
PropTypeDefault
itemsItems to be rendered within this group.readonly any[]—

GroupLabelAutocomplete.GroupLabel

An accessible label that is automatically associated with its parent group. Renders a <div>.

IconAutocomplete.Icon

An icon that indicates that the trigger button opens the popup. Renders a <span>.

InputAutocomplete.Input

A text input to search for items in the list. Renders an <input>.

Input props
PropTypeDefault
disabledWhether the component should ignore user interaction.booleanfalse
Input data attributes
Data attributeDescription
data-popup-openPresent when the corresponding popup is open.
data-pressedPresent when the input is pressed.
data-disabledPresent when the component is disabled.
data-readonlyPresent when the component is readonly.
data-popup-sideIndicates which side the corresponding popup is positioned relative to its anchor.
data-requiredPresent when the component is required.
data-validPresent when the component is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the component is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the component has been touched (when wrapped in Field.Root).
data-dirtyPresent when the component's value has changed (when wrapped in Field.Root).
data-filledPresent when the component has a value (when wrapped in Field.Root).
data-focusedPresent when the input is focused (when wrapped in Field.Root).
data-list-emptyPresent when the corresponding items list is empty.

InputGroupAutocomplete.InputGroup

A wrapper for the input and its associated controls. Renders a <div>.

InputGroup data attributes
Data attributeDescription
data-popup-openPresent when the corresponding popup is open.
data-pressedPresent when the input group is pressed.
data-disabledPresent when the component is disabled.
data-readonlyPresent when the component is readonly.
data-popup-sideIndicates which side the corresponding popup is positioned relative to its anchor.
data-validPresent when the component is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the component is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the component has been touched (when wrapped in Field.Root).
data-dirtyPresent when the component's value has changed (when wrapped in Field.Root).
data-filledPresent when the component has a value (when wrapped in Field.Root).
data-focusedPresent when the component is focused (when wrapped in Field.Root).
data-list-emptyPresent when the corresponding items list is empty.

ItemAutocomplete.Item

An individual item in the list. Renders a <div>.

Item props
PropTypeDefault
valueA unique value that identifies this item.anynull
onClickAn optional click handler for the item when selected.((event: BaseUIEvent<React.MouseEvent<HTMLDivElement, MouseEvent>>) => void)—
indexThe index of the item in the list.number—
nativeButtonWhether the component renders a native <button> element when replacing itbooleanfalse
disabledWhether the component should ignore user interaction.booleanfalse
childrenReact.ReactNode—
Item data attributes
Data attributeDescription
data-highlightedPresent when the item is highlighted.
data-disabledPresent when the item is disabled.

ListAutocomplete.List

A list container for the items. Renders a <div>.

List props
PropTypeDefault
childrenReact.ReactNode | ((item: any, index: number) => React.ReactNode)—

PopupAutocomplete.Popup

A container for the list. Renders a <div>.

Popup props
PropTypeDefault
initialFocusDetermines the element to focus when the popup is opened.boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)—
finalFocusDetermines the element to focus when the popup is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—
Popup data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-starting-stylePresent when the popup begins animating in.
data-ending-stylePresent when the popup is animating out.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
data-anchor-hiddenPresent when the anchor is hidden.
data-emptyPresent when the items list is empty.

PortalAutocomplete.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse

PositionerAutocomplete.Positioner

Positions the popup against the trigger. Renders a <div>.

Positioner props
PropTypeDefault
disableAnchorTrackingWhether to disable the popup from tracking any layout shift of its positioning anchor.booleanfalse
alignHow to align the popup relative to the specified side.Align'center'
alignOffsetAdditional offset along the alignment axis in pixels.number | OffsetFunction0
sideWhich side of the anchor element to align the popup against.Side'bottom'
sideOffsetDistance between the anchor and the popup in pixels.number | OffsetFunction0
arrowPaddingMinimum distance to maintain between the arrow and the edges of the popup.number5
anchorAn element to position the popup against.Element | VirtualElement | React.RefObject<Element | null> | (() => Element | VirtualElement | null) | null—
collisionAvoidanceDetermines how to handle collisions when positioning the popup.CollisionAvoidance—
collisionBoundaryAn element or a rectangle that delimits the area that the popup is confined to.Boundary'clipping-ancestors'
collisionPaddingAdditional space to maintain from the edge of the collision boundary.Padding5
stickyWhether to maintain the popup in the viewport afterbooleanfalse
positionMethodDetermines which CSS position property to use."absolute" | "fixed"'absolute'
Positioner data attributes
Data attributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-anchor-hiddenPresent when the anchor is hidden.
data-sideIndicates which side the popup is positioned relative to the trigger.
data-alignIndicates how the popup is aligned relative to specified side.
data-emptyPresent when the items list is empty.
Positioner CSS variables
CSS variableDescription
--available-widthThe available width between the trigger and the edge of the viewport.
--available-heightThe available height between the trigger and the edge of the viewport.
--anchor-widthThe anchor's width.
--anchor-heightThe anchor's height.
--transform-originThe coordinates that this element is anchored to. Used for animations and transitions.

RootAutocomplete.Root

Groups all parts of the autocomplete. Doesn’t render its own element.

Root props
PropTypeDefault
nameIdentifies the field when a form is submitted.string—
defaultValueThe uncontrolled input value of the autocomplete when it's initially rendered.string | number | readonly string[]—
valueThe input value of the autocomplete.string | number | readonly string[]—
onValueChangeEvent handler called when the input value of the autocomplete changes.((value: string, eventDetails: AutocompleteRootChangeEventDetails) => void)—
defaultOpenWhether the popup is initially open.booleanfalse
openWhether the popup is currently open.boolean—
onOpenChangeEvent handler called when the popup is opened or closed.((open: boolean, eventDetails: AutocompleteRootOpenChangeEventDetails) => void)—
autoHighlightWhether the first matching item is highlighted automatically.boolean | "always"false
keepHighlightWhether the highlighted item should be preserved when the pointer leaves the list.booleanfalse
highlightItemOnHoverWhether moving the pointer over items should highlight them.booleantrue
actionsRefA ref to imperative actions.React.RefObject<AutocompleteRootActions | null>—
filterFilter function used to match items against the input query.((item: ItemValue, query: string, itemToString?: ((item: ItemValue) => string) | undefined) => boolean) | null—
filteredItemsFiltered items to display in the list.readonly ItemValue[] | readonly Group<ItemValue>[]—
formIdentifies the form that owns the internal input.string—
gridWhether list items are presented in a grid layout.booleanfalse
inlineWhether the list is rendered inline without using the component's own popup.booleanfalse
itemToStringValueWhen the item values are objects (<Autocomplete.Item value={object}>), this function converts the object value to a string representation for both display in the input and form submission.((itemValue: ItemValue) => string)—
itemsThe items to be displayed in the list.readonly ItemValue[] | readonly Group<ItemValue>[]—
limitThe maximum number of items to display in the list.number-1
localeThe locale to use for string comparison.Intl.LocalesArgument—
loopFocusWhether to loop keyboard focus back to the input when the end of the list is reached while using the arrow keys.booleantrue
modalDetermines if the popup enters a modal state when open.booleanfalse
modeControls how the autocomplete behaves with respect to list filtering and inline autocompletion."none" | "inline" | "list" | "both"'list'
onItemHighlightedCallback fired when an item is highlighted or unhighlighted.((highlightedValue: ItemValue | undefined, eventDetails: AriaCombobox.HighlightEventDetails) => void)—
onOpenChangeCompleteEvent handler called after any animations complete when the popup is opened or closed.((open: boolean) => void)—
openOnInputClickWhether the popup opens when clicking the input.booleanfalse
submitOnItemClickWhether clicking an item should submit the autocomplete's owning form.booleanfalse
virtualizedWhether the items are being externally virtualized.booleanfalse
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to choose a different option from the popup.booleanfalse
requiredWhether the user must choose a value before submitting a form.booleanfalse
inputRefA ref to the hidden input element.React.Ref<HTMLInputElement>—
idThe id of the component.string—
childrenReact.ReactNode—
isItemDisabledWhether the item at index is disabled, for items that are not mounted (a virtualized list).((itemValue: ItemValue, index: number) => boolean)—

RowAutocomplete.Row

Displays a single row of items in a grid list. Enable grid on the root component to turn the listbox into a grid. Renders a <div>.

SeparatorAutocomplete.Separator

A visual separator between items or groups. Renders a <div>.

Separator props
PropTypeDefault
orientationThe orientation of the separator.Orientation'horizontal'
Separator data attributes
Data attributeDescription
data-orientationIndicates the orientation of the separator.

StatusAutocomplete.Status

Displays a status message whose content changes are announced politely to screen readers. Useful for conveying the status of an asynchronously loaded list. This component's root element must remain mounted in the DOM to announce changes consistently across screen readers. Avoid hiding or removing the component itself with display: none, hidden, aria-hidden, or conditional rendering. Prefer updating or conditionally rendering its children instead. Renders a <div>.

TriggerAutocomplete.Trigger

A button that opens the popup. Renders a <button>.

Trigger props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
disabledWhether the component should ignore user interaction.booleanfalse
Trigger data attributes
Data attributeDescription
data-popup-openPresent when the corresponding popup is open.
data-pressedPresent when the trigger is pressed.
data-disabledPresent when the component is disabled.
data-readonlyPresent when the component is readonly.
data-popup-sideIndicates which side the corresponding popup is positioned relative to its anchor.
data-requiredPresent when the component is required.
data-validPresent when the component is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the component is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the component has been touched (when wrapped in Field.Root).
data-dirtyPresent when the component's value has changed (when wrapped in Field.Root).
data-filledPresent when the component has a value (when wrapped in Field.Root).
data-focusedPresent when the trigger is focused (when wrapped in Field.Root).
data-list-emptyPresent when the corresponding items list is empty.

ValueAutocomplete.Value

The current value of the autocomplete. Doesn’t render its own element.

Value props
PropTypeDefault
childrenReact.ReactNode | ((value: string) => React.ReactNode)—

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ArrowDownOpens the list; with autoHighlight, highlights the first itemInput
EnterFills the input with the highlighted item and runs its click handlerItem
EnterWith no item highlighted, submits the form the input belongs toInput
EscapeCloses the list and keeps what was typedInput
TabCloses the list and moves onInput

Accessibility

What the parts set
PartSetsSo that
Inputrole="combobox", aria-expanded, aria-haspopup, aria-controls to the list while opena screen reader says the input has suggestions and whether they are shown
Inputaria-autocomplete from modea screen reader knows whether text is completed inline, listed, or both
Inputaria-activedescendant to the highlighted itemthe highlight is read while focus stays in the input
Listrole="listbox" (role="grid" with grid)the suggestions are announced as a list of options
Itemrole="option"each suggestion is read as an option
Statusrole="status", aria-live="polite"loading and result counts are announced without moving focus
Emptyrole="status", aria-live="polite""no results" is announced as it appears
Iconaria-hidden="true"the decorative icon is not read

Focus order

  1. Focus stays in Input the whole time; arrow keys move a highlight in the list, not focus.
  2. Clear and Trigger, when rendered, are not tab stops of their own.
  3. Tab closes the list and moves to the next element after the input.