AutoComplete

free

AutoComplete wraps an Input with a floating dropdown that filters a list of options as the user types.

@floating-ui/react
Preview
Dark
import { useState } from 'react';
import AutoComplete from '@/components/composites/AutoComplete';

const countries = [
  'Australia', 'Belgium', 'Brazil', 'Canada', 'China',
  'Denmark', 'Egypt', 'Finland', 'France', 'Germany',
  'India', 'Indonesia', 'Italy', 'Japan', 'Mexico',
  'Netherlands', 'New Zealand', 'Norway', 'Poland', 'Portugal',
  'Singapore', 'South Korea', 'Spain', 'Sweden', 'Switzerland',
  'Turkey', 'United Kingdom', 'United States',
];

export default function UsageDemo() {
  const [value, setValue] = useState('');

  return (
    <div>
      <AutoComplete
        data={countries}
        optionKey={(item) => item}
        value={value}
        onInputChange={setValue}
        onOptionSelected={(item) => setValue(item)}
        renderOption={(item) => (
          <span className="text-sm">{item}</span>
        )}
        placeholder="Search countries"
        className="w-full"
      />
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add AutoComplete

Examples

Custom Render

Use renderOption to display rich content inside each dropdown item. This example shows an avatar, name, and email for each person.

Preview
Dark
import { useState } from 'react';
import AutoComplete from '@/components/composites/AutoComplete';
import Avatar from '@/components/ui/Avatar';

type Person = {
  name: string;
  email: string;
  avatar: string;
};

const people: Person[] = [
  { name: 'Aria Patel', email: 'aria@acme.io', avatar: '/img/avatars/thumb-1.jpg' },
  { name: 'Diego Ramos', email: 'diego@acme.io', avatar: '/img/avatars/thumb-2.jpg' },
  { name: 'Mei Lin', email: 'mei@acme.io', avatar: '/img/avatars/thumb-3.jpg' },
  { name: 'Noah Becker', email: 'noah@acme.io', avatar: '/img/avatars/thumb-4.jpg' },
  { name: 'Priya Nair', email: 'priya@acme.io', avatar: '/img/avatars/thumb-5.jpg' },
  { name: 'Lucas Ferreira', email: 'lucas@acme.io', avatar: '/img/avatars/thumb-6.jpg' },
  { name: 'Hana Sato', email: 'hana@acme.io', avatar: '/img/avatars/thumb-7.jpg' },
  { name: 'Owen Clarke', email: 'owen@acme.io', avatar: '/img/avatars/thumb-8.jpg' },
];

export default function CustomRenderDemo() {
  const [value, setValue] = useState('');

  return (
    <div>
      <AutoComplete
        data={people}
        optionKey={(p) => p.name}
        value={value}
        onInputChange={setValue}
        onOptionSelected={(p) => setValue(p.name)}
        renderOption={(p) => (
          <div className="flex items-center gap-2">
            <Avatar shape="round" src={p.avatar} size="sm" />
            <div className="min-w-0">
              <div className="text-sm font-medium">{p.name}</div>
              <div className="text-xs text-muted-foreground truncate">
                {p.email}
              </div>
            </div>
          </div>
        )}
        placeholder="Search people"
        className="w-full"
      />
    </div>
  );
}

Controlled

value, onInputChange, and onOptionSelected give full external control over the input text and the selected option. The component never writes internal state back to the input.

Preview
Dark
import { useState } from 'react';
import AutoComplete from '@/components/composites/AutoComplete';
import Tag from '@/components/ui/Tag';

const frameworks = [
  'Next.js', 'Remix', 'Astro', 'SvelteKit', 'SolidStart',
  'Nuxt', 'Gatsby', 'Vite', 'TanStack Start', 'Qwik City',
];

export default function ControlledDemo() {
  const [value, setValue] = useState('');
  const [selected, setSelected] = useState<string | null>(null);

  return (
    <div className="space-y-4">
      <AutoComplete
        data={frameworks}
        optionKey={(f) => f}
        value={value}
        onInputChange={setValue}
        onOptionSelected={(f) => {
          setValue(f);
          setSelected(f);
        }}
        renderOption={(f) => (
          <span className="text-sm">{f}</span>
        )}
        placeholder="Pick a framework"
        className="w-full"
      />
      {selected && (
        <div className="text-sm text-muted-foreground">
          Selected:{' '}
          <Tag>{selected}</Tag>
        </div>
      )}
    </div>
  );
}

Size

The input size is inherited from the size prop, which follows the standard ControlSize scale.

Preview
Dark
import { useState } from 'react';
import AutoComplete from '@/components/composites/AutoComplete';

const colors = [
  'Crimson', 'Indigo', 'Teal', 'Amber', 'Slate',
  'Rose', 'Emerald', 'Violet', 'Sky', 'Orange',
];

export default function SizeDemo() {
  const [sm, setSm] = useState('');
  const [md, setMd] = useState('');
  const [lg, setLg] = useState('');

  return (
    <div className="space-y-4">
      <AutoComplete
        data={colors}
        optionKey={(c) => c}
        value={sm}
        onInputChange={setSm}
        onOptionSelected={(c) => setSm(c)}
        renderOption={(c) => <span className="text-sm">{c}</span>}
        placeholder="Small"
        size="sm"
        className="w-full"
      />
      <AutoComplete
        data={colors}
        optionKey={(c) => c}
        value={md}
        onInputChange={setMd}
        onOptionSelected={(c) => setMd(c)}
        renderOption={(c) => <span className="text-sm">{c}</span>}
        placeholder="Medium (default)"
        className="w-full"
      />
      <AutoComplete
        data={colors}
        optionKey={(c) => c}
        value={lg}
        onInputChange={setLg}
        onOptionSelected={(c) => setLg(c)}
        renderOption={(c) => <span className="text-sm">{c}</span>}
        placeholder="Large"
        size="lg"
        className="w-full"
      />
    </div>
  );
}

Disabled

The disabled state prevents input and blocks the dropdown from opening.

Preview
Dark
import AutoComplete from '@/components/composites/AutoComplete';

const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];

export default function DisabledDemo() {
  return (
    <div>
      <AutoComplete
        data={items}
        optionKey={(i) => i}
        value="Apple"
        onInputChange={() => {}}
        onOptionSelected={() => {}}
        renderOption={(i) => <span className="text-sm">{i}</span>}
        placeholder="Disabled"
        disabled
        className="w-full"
      />
    </div>
  );
}

Async

Simulate a remote search with a loading state. The remote function owns the filtering, so the component renders the returned results directly.

Preview
Dark
import { useEffect, useRef, useState } from 'react';
import AutoComplete from '@/components/composites/AutoComplete';
import Spinner from '@/components/ui/Spinner';

type Product = {
  name: string;
  sku: string;
  category: string;
};

const productIndex: Product[] = [
  { name: 'Wireless Mouse', sku: 'ACC-1024', category: 'Peripherals' },
  { name: 'Mechanical Keyboard', sku: 'ACC-2048', category: 'Peripherals' },
  { name: 'USB-C Hub', sku: 'DOC-3100', category: 'Docking' },
  { name: 'Monitor Stand', sku: 'DSK-4400', category: 'Workspace' },
  { name: 'Desk Lamp', sku: 'LIT-1810', category: 'Workspace' },
  { name: 'Webcam HD', sku: 'CAM-2200', category: 'Video' },
  { name: 'Noise Cancelling Headphones', sku: 'AUD-5100', category: 'Audio' },
  { name: 'Laptop Sleeve', sku: 'BAG-1280', category: 'Accessories' },
  { name: 'External SSD', sku: 'DRV-9000', category: 'Storage' },
  { name: 'Graphics Tablet', sku: 'CRT-7200', category: 'Creative' },
  { name: 'Cable Management Kit', sku: 'CAB-1180', category: 'Workspace' },
  { name: 'Power Strip', sku: 'PWR-6600', category: 'Power' },
  { name: 'Ergonomic Chair', sku: 'CHR-7320', category: 'Furniture' },
  { name: 'Footrest', sku: 'FTS-2060', category: 'Furniture' },
  { name: 'Blue Light Glasses', sku: 'EYE-3300', category: 'Accessories' },
  { name: 'Microphone', sku: 'AUD-4200', category: 'Audio' },
];

function searchProducts(query: string): Promise<Product[]> {
  return new Promise((resolve) => {
    setTimeout(() => {
      const normalizedQuery = query.trim().toLowerCase();
      const results = productIndex.filter((product) =>
        [product.name, product.sku, product.category].some((field) =>
          field.toLowerCase().includes(normalizedQuery)
        )
      );
      resolve(results.slice(0, 6));
    }, 500);
  });
}

export default function AsyncDemo() {
  const [value, setValue] = useState('');
  const [results, setResults] = useState<Product[]>([]);
  const [loading, setLoading] = useState(false);
  const requestIdRef = useRef(0);

  useEffect(() => {
    const query = value.trim();
    const requestId = requestIdRef.current + 1;
    requestIdRef.current = requestId;

    if (query.length < 2) {
      setResults([]);
      setLoading(false);
      return;
    }

    setResults([]);
    setLoading(true);

    searchProducts(query).then((nextResults) => {
      if (requestIdRef.current !== requestId) {
        return;
      }

      setResults(nextResults);
      setLoading(false);
    });
  }, [value]);

  return (
    <div>
      <AutoComplete
        data={results}
        optionKey={(product) => product.name}
        filterOptions={false}
        value={value}
        onInputChange={setValue}
        onOptionSelected={(product) => setValue(product.name)}
        renderOption={(product) => product.name}
        placeholder="Search products by name or SKU"
        className="w-full"
        suffix={
          <Spinner
            className={
              loading ? 'opacity-100' : 'opacity-0'
            }
          />
        }
      />
    </div>
  );
}

API

PropDescriptionTypeDefault
dataArray of options to filter and display.T[]
optionKeyFunction that extracts the display string from an option. Used for filtering and setting the input value on selection.(obj: T) => string
filterOptionsWhether to apply the built-in text filter before rendering data. Disable this when data is already filtered by a remote search.booleantrue
valueControlled input value.string
onInputChangeCallback when the input text changes.(value: string) => void
onOptionSelectedCallback when an option is selected (click or Enter).(option: T) => void
renderOptionCustom render function for each dropdown item.(option: T) => ReactNode
sizeInput size.'sm' | 'md' | 'lg''md'
disabledPrevents input and blocks the dropdown.booleanfalse
placeholderInput placeholder text.string
prefixContent rendered before the input text.string | ReactNode
suffixContent rendered after the input text.string | ReactNode
invalidMarks the input as invalid (form integration).booleanfalse
classNameAdditional classes on the input element.string

Types

type AutoCompleteProps<T> = Omit<InputProps, 'onChange'> & {
  data: Array<T>;
  optionKey: (obj: T) => string;
  filterOptions?: boolean;
  value: string;
  onInputChange: (value: string) => void;
  onOptionSelected: (option: T) => void;
  renderOption: (option: T) => ReactNode;
};