Skip to content

Repository files navigation

Autocomplete

A generic, accessible, keyboard-navigable multi-select autocomplete component built from scratch with React and TypeScript.

Features

  • Fully generic — works with string, objects, or any custom type via getLabel / getKey props
  • Dynamic item creation — "Create X" option appears when input doesn't match any suggestion
  • Keyboard navigation to move, Enter to select, Backspace to remove last tag, Escape to close
  • Controlled component — state lives in the parent, component only reports changes via onChange
  • WAI-ARIA compliantrole="combobox", aria-expanded, aria-activedescendant, role="listbox", role="option"
  • Click outside to close — via mousedown listener on document
  • No external autocomplete libraries used

Getting Started

npm install
npm run dev

Component API

<Autocomplete<T>
    options={T[]}                         // full list of available options
    tags={T[]}                            // currently selected items (controlled)
    onChange={(selected: T[]) => void}    // called on every change
    getLabel={(item: T) => string}        // how to display an item
    getKey={(item: T) => string}          // unique identifier for an item
    placeholder?: string
allowCreate?: boolean                 // show "Create X" when no match found
onCreate?: (inputValue: string) => T  // how to construct a new T from typed text
    />
Prop Type Required Description
options T[] List of available options
tags T[] Currently selected items
onChange (selected: T[]) => void Fired on every selection or removal
getLabel (item: T) => string Extracts display text from an item
getKey (item: T) => string Extracts unique key from an item
placeholder string Input placeholder text
allowCreate boolean Enables creating items not in the list
onCreate (val: string) => T Builds a new T from typed text

Usage

Strings

const LANGUAGES = ["English", "French", "German", "Polish"];
const [langs, setLangs] = useState<string[]>([]);

<Autocomplete<string>
    options={LANGUAGES}
    tags={langs}
    onChange={setLangs}
    getLabel={s => s}
    getKey={s => s}
    allowCreate
    onCreate={val => val}
    placeholder="Search languages..."
/>

Custom objects

interface Address {
    id: string;
    streetName: string;
    city: string;
}

const ADDRESSES: Address[] = [
    { id: "1", streetName: "Wawel 5", city: "Krakow" },
    { id: "2", streetName: "Unter den Linden 1", city: "Berlin" },
    { id: "3", streetName: "Piazza del Colosseo", city: "Rome" },
];

const [addresses, setAddresses] = useState<Address[]>([]);

<Autocomplete<Address>
    options={ADDRESSES}
    tags={addresses}
    onChange={setAddresses}
    getLabel={a => `${a.streetName}, ${a.city}`}
    getKey={a => a.id}
    placeholder="Search addresses..."
/>

The component is identical in both cases — only getLabel and getKey change.

Project Structure

src/
  Autocomplete.tsx   — generic component + AutocompleteOptions interface
  Tag.tsx            — selected item chip with remove button
  Option.tsx         — single dropdown row
  InputArea.tsx      — controlled input with full ARIA attributes
  App.tsx            — demo: Languages (strings) + Addresses (objects)
  App.css            — all styles

Key Design Decisions

getLabel / getKey as functions, not strings A labelKey: "name" prop only works for flat objects with known fields. Functions handle any shape of data — nested properties, computed values like `${streetName}, ${city}`, formatted dates, anything.

onMouseDown instead of onClick on options onClick fires after onBlur. When clicking an option, the input loses focus first — closing the dropdown before the click registers. onMouseDown fires before onBlur, so the selection always goes through.

Options never stored in state Removing selected items from an options state array makes them unrecoverable if a tag is removed. Instead, options is a static prop and already-selected items are excluded at render time using derived state.

Controlled component pattern The component holds no knowledge of which items are selected — that state lives in the parent. This follows the same pattern as a standard <input value={x} onChange={...} /> and makes the component predictable and easy to integrate.

Accessibility

Attribute Element Purpose
role="combobox" <input> Identifies the widget type
aria-expanded <input> Reflects dropdown open/closed state
aria-haspopup="listbox" <input> Signals what the popup is
aria-autocomplete="list" <input> Describes autocomplete behaviour
aria-activedescendant <input> Points to the currently highlighted option
role="listbox" <ul> Identifies the dropdown list
role="option" <li> Identifies each item
aria-selected <li> Reflects keyboard highlight state
aria-label remove <button> e.g. "remove English"

Tech Stack

  • React 18
  • TypeScript
  • Vite
  • Zero external autocomplete libraries

About

UI component for autocomplete

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages