Autocomplete

An input that suggests options as you type.

"use client";

import { useState } from "react";
import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompletePortal,
  AutocompletePositioner,
} from "@/components/ui/autocomplete/autocomplete";
import { Badge } from "@/components/ui/badge/badge";
import styles from "./autocomplete-demo.module.css";

type Question = {
  value: string;
  question: string;
  category: string;
};

const questions: Question[] = [
  {
    value: "reset-password",
    question: "How do I reset my password?",
    category: "Account",
  },
  {
    value: "payment-methods",
    question: "What payment methods do you accept?",
    category: "Billing",
  },
  {
    value: "cancel-subscription",
    question: "How do I cancel my subscription?",
    category: "Billing",
  },
  { value: "export-data", question: "Can I export my data?", category: "Data" },
  {
    value: "api-access",
    question: "How do I get API access?",
    category: "Developer",
  },
  {
    value: "contact-support",
    question: "How do I contact support?",
    category: "Support",
  },
  {
    value: "refund-policy",
    question: "What is your refund policy?",
    category: "Billing",
  },
  {
    value: "data-privacy",
    question: "How is my data protected?",
    category: "Privacy",
  },
  {
    value: "team-members",
    question: "How do I add team members?",
    category: "Account",
  },
  {
    value: "integrations",
    question: "What integrations are available?",
    category: "Features",
  },
];

export default function AutocompleteDemo() {
  const [value, setValue] = useState("");

  return (
    <div className={styles.container}>
      <label className={styles.label} htmlFor="ac-input">
        Search for help or ask a question
      </label>

      <Autocomplete
        items={questions}
        itemToStringValue={(item) => (item as Question).question}
        onValueChange={setValue}
        value={value}
      >
        <AutocompleteInput className={styles.input} id="ac-input" placeholder="Type your question or search FAQs..." />

        <AutocompletePortal>
          <AutocompletePositioner>
            <AutocompletePopup>
              <AutocompleteEmpty>No matching questions found. Type your own question!</AutocompleteEmpty>
              <AutocompleteList>
                {(question: Question) => (
                  <AutocompleteItem key={question.value} value={question}>
                    <div className={styles.itemContainer}>
                      <div className={styles.itemInfo}>
                        <div className={styles.itemName}>{question.question}</div>
                      </div>
                      <Badge className={styles.badge} size="sm" variant="outline">
                        {question.category}
                      </Badge>
                    </div>
                  </AutocompleteItem>
                )}
              </AutocompleteList>
            </AutocompletePopup>
          </AutocompletePositioner>
        </AutocompletePortal>
      </Autocomplete>
    </div>
  );
}

npx shadcn@latest add @roiui/autocomplete
npx shadcn@latest add @roiui/autocomplete-tailwind

anatomy
<Autocomplete>
  <AutocompleteInput />
  <AutocompleteTrigger />
  <AutocompleteIcon />
  <AutocompleteClear />
  <AutocompleteValue />

  <AutocompletePortal>
      <AutocompleteBackdrop />
      <AutocompletePositioner>
          <AutocompletePopup>
              <AutocompleteArrow />
              <AutocompleteStatus />
              <AutocompleteEmpty />

              <AutocompleteList>
                  <AutocompleteRow>
                      <AutocompleteItem />
                  </AutocompleteRow>
                  <AutocompleteSeparator />
                  <AutocompleteGroup>
                      <AutocompleteGroupLabel />
                      <AutocompleteCollection />
                  </AutocompleteGroup>
              </AutocompleteList>
          </AutocompletePopup>
      </AutocompletePositioner>
  </AutocompletePortal>
</Autocomplete>

"use client";

import { useState } from "react";
import {
  Autocomplete,
  AutocompleteClear,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompletePortal,
  AutocompletePositioner,
} from "@/components/ui/autocomplete/autocomplete";
import styles from "./autocomplete-clearable.module.css";

const frameworks = [
  { value: "next", label: "Next.js" },
  { value: "remix", label: "Remix" },
  { value: "astro", label: "Astro" },
  { value: "nuxt", label: "Nuxt" },
  { value: "sveltekit", label: "SvelteKit" },
  { value: "gatsby", label: "Gatsby" },
];

export default function AutocompleteClearable() {
  const [value, setValue] = useState("");

  return (
    <div className={styles.container}>
      <label className={styles.label} htmlFor="clearable-input">
        Select a framework
      </label>

      <Autocomplete
        items={frameworks}
        itemToStringValue={(item) => (item as (typeof frameworks)[0]).label}
        onValueChange={setValue}
        value={value}
      >
        <div className={styles.inputWrapper}>
          <AutocompleteInput className={styles.input} id="clearable-input" placeholder="Search frameworks..." />
          <AutocompleteClear />
        </div>

        <AutocompletePortal>
          <AutocompletePositioner>
            <AutocompletePopup className={styles.popup}>
              <AutocompleteEmpty>No frameworks found.</AutocompleteEmpty>
              <AutocompleteList>
                {(item: (typeof frameworks)[0]) => (
                  <AutocompleteItem key={item.value} value={item}>
                    {item.label}
                  </AutocompleteItem>
                )}
              </AutocompleteList>
            </AutocompletePopup>
          </AutocompletePositioner>
        </AutocompletePortal>
      </Autocomplete>
    </div>
  );
}

"use client";

import { useState } from "react";
import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompletePortal,
  AutocompletePositioner,
} from "@/components/ui/autocomplete/autocomplete";
import styles from "./autocomplete-auto-highlight.module.css";

const countries = [
  { value: "us", label: "United States" },
  { value: "uk", label: "United Kingdom" },
  { value: "ca", label: "Canada" },
  { value: "au", label: "Australia" },
  { value: "de", label: "Germany" },
  { value: "fr", label: "France" },
  { value: "jp", label: "Japan" },
  { value: "br", label: "Brazil" },
];

export default function AutocompleteAutoHighlight() {
  const [value, setValue] = useState("");

  return (
    <div className={styles.container}>
      <label className={styles.label} htmlFor="auto-highlight-input">
        Select a country
      </label>

      <Autocomplete
        autoHighlight
        items={countries}
        itemToStringValue={(item) => (item as (typeof countries)[0]).label}
        onValueChange={setValue}
        value={value}
      >
        <AutocompleteInput className={styles.input} id="auto-highlight-input" placeholder="Start typing..." />

        <AutocompletePortal>
          <AutocompletePositioner>
            <AutocompletePopup>
              <AutocompleteEmpty>No countries found.</AutocompleteEmpty>
              <AutocompleteList>
                {(item: (typeof countries)[0]) => (
                  <AutocompleteItem key={item.value} value={item}>
                    {item.label}
                  </AutocompleteItem>
                )}
              </AutocompleteList>
            </AutocompletePopup>
          </AutocompletePositioner>
        </AutocompletePortal>
      </Autocomplete>
    </div>
  );
}

"use client";

import { matchSorter } from "match-sorter";
import type React from "react";
import { useState } from "react";
import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompletePortal,
  AutocompletePositioner,
  AutocompleteValue,
} from "@/components/ui/autocomplete/autocomplete";
import styles from "./autocomplete-fuzzy.module.css";

interface Question {
  value: string;
  question: string;
  category: string;
}

const questions: Question[] = [
  {
    value: "reset-password",
    question: "How do I reset my password?",
    category: "Account",
  },
  {
    value: "payment-methods",
    question: "What payment methods do you accept?",
    category: "Billing",
  },
  {
    value: "cancel-subscription",
    question: "How do I cancel my subscription?",
    category: "Billing",
  },
  {
    value: "export-data",
    question: "Can I export my data?",
    category: "Data",
  },
  {
    value: "api-access",
    question: "How do I get API access?",
    category: "Developer",
  },
  {
    value: "contact-support",
    question: "How do I contact support?",
    category: "Support",
  },
  {
    value: "refund-policy",
    question: "What is your refund policy?",
    category: "Billing",
  },
  {
    value: "data-privacy",
    question: "How is my data protected?",
    category: "Privacy",
  },
  {
    value: "team-members",
    question: "How do I add team members?",
    category: "Account",
  },
  {
    value: "integrations",
    question: "What integrations are available?",
    category: "Features",
  },
];

function highlightText(text: string, query: string): React.ReactNode {
  const trimmed = query.trim().toLowerCase();
  if (!trimmed) {
    return text;
  }

  const textLower = text.toLowerCase();
  const result: React.ReactNode[] = [];
  let queryIndex = 0;

  for (let i = 0; i < text.length; i++) {
    if (queryIndex < trimmed.length && textLower[i] === trimmed[queryIndex]) {
      result.push(
        <mark className={styles.highlight} key={i}>
          {text[i]}
        </mark>
      );
      queryIndex++;
    } else {
      result.push(text[i]);
    }
  }

  return result;
}

function fuzzyFilter(itemValue: unknown, query: string): boolean {
  if (!query) {
    return true;
  }

  const item = itemValue as Question;
  const results = matchSorter([item], query, {
    keys: [
      "question",
      "category",
      { key: "question", threshold: matchSorter.rankings.CONTAINS },
      { key: "category", threshold: matchSorter.rankings.WORD_STARTS_WITH },
    ],
  });

  return results.length > 0;
}

export default function AutocompleteFuzzy() {
  const [value, setValue] = useState("");

  return (
    <div className={styles.container}>
      <label className={styles.label} htmlFor="fuzzy-input">
        Search for help
      </label>

      <Autocomplete
        filter={fuzzyFilter}
        items={questions}
        itemToStringValue={(item) => (item as Question).question}
        onValueChange={setValue}
        value={value}
      >
        <AutocompleteInput className={styles.input} id="fuzzy-input" placeholder="Try 'billing' or 'password'..." />

        <AutocompletePortal>
          <AutocompletePositioner>
            <AutocompletePopup>
              <AutocompleteEmpty className={styles.empty}>
                No results found for "<AutocompleteValue />"
              </AutocompleteEmpty>
              <AutocompleteList>
                {(item: Question) => (
                  <AutocompleteItem className={styles.item} key={item.value} value={item}>
                    <AutocompleteValue>
                      {(inputValue) => (
                        <div className={styles.itemContent}>
                          <div className={styles.itemQuestion}>{highlightText(item.question, inputValue)}</div>
                          <div className={styles.itemCategory}>{highlightText(item.category, inputValue)}</div>
                        </div>
                      )}
                    </AutocompleteValue>
                  </AutocompleteItem>
                )}
              </AutocompleteList>
            </AutocompletePopup>
          </AutocompletePositioner>
        </AutocompletePortal>
      </Autocomplete>
    </div>
  );
}

"use client";

import { useState } from "react";
import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteGroup,
  AutocompleteGroupLabel,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompletePortal,
  AutocompletePositioner,
} from "@/components/ui/autocomplete/autocomplete";
import styles from "./autocomplete-grouped.module.css";

type Item = {
  value: string;
  label: string;
};

type Group = {
  value: string;
  items: Item[];
};

const groups: Group[] = [
  {
    value: "Fruits",
    items: [
      { value: "apple", label: "Apple" },
      { value: "banana", label: "Banana" },
      { value: "orange", label: "Orange" },
    ],
  },
  {
    value: "Vegetables",
    items: [
      { value: "carrot", label: "Carrot" },
      { value: "broccoli", label: "Broccoli" },
      { value: "spinach", label: "Spinach" },
    ],
  },
  {
    value: "Dairy",
    items: [
      { value: "milk", label: "Milk" },
      { value: "cheese", label: "Cheese" },
      { value: "yogurt", label: "Yogurt" },
    ],
  },
];

export default function AutocompleteGrouped() {
  const [value, setValue] = useState("");

  return (
    <div className={styles.container}>
      <label className={styles.label} htmlFor="grouped-input">
        Select an item
      </label>

      <Autocomplete
        items={groups}
        itemToStringValue={(item) => (item as Item).label}
        onValueChange={setValue}
        value={value}
      >
        <AutocompleteInput className={styles.input} id="grouped-input" placeholder="Search items..." />

        <AutocompletePortal>
          <AutocompletePositioner>
            <AutocompletePopup>
              <AutocompleteEmpty>No items found.</AutocompleteEmpty>
              <AutocompleteList>
                {(group: Group) => (
                  <AutocompleteGroup key={group.value}>
                    <AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
                    {group.items.map((item) => (
                      <AutocompleteItem key={item.value} value={item}>
                        {item.label}
                      </AutocompleteItem>
                    ))}
                  </AutocompleteGroup>
                )}
              </AutocompleteList>
            </AutocompletePopup>
          </AutocompletePositioner>
        </AutocompletePortal>
      </Autocomplete>
    </div>
  );
}