> ## Documentation Index
> Fetch the complete documentation index at: https://www.logo.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a company search autocomplete

> Let users pick their company by typing a name. Capture a clean name, domain, and logo in one step with the Logo.dev Search API.

export const CompanyAutocompleteDemo = () => {
  const TOKEN = "live_6a1a28fd-6420-4492-aeb0-b297461d9de2";
  const COMPANIES = [{
    name: "Adobe",
    domain: "adobe.com"
  }, {
    name: "Airbnb",
    domain: "airbnb.com"
  }, {
    name: "Amazon",
    domain: "amazon.com"
  }, {
    name: "Figma",
    domain: "figma.com"
  }, {
    name: "GitHub",
    domain: "github.com"
  }, {
    name: "Google",
    domain: "google.com"
  }, {
    name: "Linear",
    domain: "linear.app"
  }, {
    name: "Netflix",
    domain: "netflix.com"
  }, {
    name: "Nike",
    domain: "nike.com"
  }, {
    name: "Nintendo",
    domain: "nintendo.com"
  }, {
    name: "Notion",
    domain: "notion.so"
  }, {
    name: "OpenAI",
    domain: "openai.com"
  }, {
    name: "Shopify",
    domain: "shopify.com"
  }, {
    name: "Slack",
    domain: "slack.com"
  }, {
    name: "Spotify",
    domain: "spotify.com"
  }, {
    name: "Stripe",
    domain: "stripe.com"
  }, {
    name: "Uber",
    domain: "uber.com"
  }, {
    name: "Vercel",
    domain: "vercel.com"
  }];
  const [query, setQuery] = useState("");
  const [open, setOpen] = useState(false);
  const [active, setActive] = useState(0);
  const [selected, setSelected] = useState(null);
  const results = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) {
      return [];
    }
    return COMPANIES.filter(c => c.name.toLowerCase().includes(q) || c.domain.includes(q)).slice(0, 6);
  }, [query]);
  const logoSrc = domain => `https://img.logo.dev/${domain}?token=${TOKEN}&format=webp&retina=true&size=64`;
  const select = company => {
    setSelected(company);
    setQuery(company.name);
    setOpen(false);
  };
  const onKeyDown = e => {
    if (!(open && results.length)) {
      return;
    }
    if (e.key === "ArrowDown") {
      e.preventDefault();
      setActive(i => (i + 1) % results.length);
    } else if (e.key === "ArrowUp") {
      e.preventDefault();
      setActive(i => (i - 1 + results.length) % results.length);
    } else if (e.key === "Enter") {
      e.preventDefault();
      select(results[active]);
    } else if (e.key === "Escape") {
      setOpen(false);
    }
  };
  return <div className="not-prose my-8 rounded-2xl border border-zinc-950/10 bg-gradient-to-br from-zinc-50 to-white p-6 shadow-sm sm:p-8 dark:border-white/10 dark:from-zinc-900 dark:to-zinc-950">
      <div className="mx-auto max-w-md">
        <label className="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300" htmlFor="company-autocomplete-input">
          Company
        </label>
        <div className="relative">
          <div className="pointer-events-none absolute inset-y-0 left-3 flex items-center">
            {selected ? <img alt="" className="h-6 w-6 rounded object-contain" height="24" src={logoSrc(selected.domain)} width="24" /> : <svg aria-hidden="true" className="h-5 w-5 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} />
              </svg>}
          </div>
          <input aria-activedescendant={open && results.length > 0 ? `company-autocomplete-option-${active}` : undefined} aria-autocomplete="list" aria-controls="company-autocomplete-listbox" aria-expanded={open && results.length > 0} autoComplete="off" className="w-full rounded-xl border border-zinc-950/10 bg-white py-3 pl-12 pr-4 text-base text-zinc-950 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-zinc-950/20 dark:border-white/20 dark:bg-zinc-900 dark:text-white dark:focus:ring-white/20" id="company-autocomplete-input" onBlur={() => setOpen(false)} onChange={e => {
    setQuery(e.target.value);
    setSelected(null);
    setActive(0);
    setOpen(true);
  }} onFocus={() => setOpen(true)} onKeyDown={onKeyDown} placeholder="Type a company name…" role="combobox" type="text" value={query} />
          {open && results.length > 0 && <ul className="absolute z-10 mt-2 w-full overflow-hidden rounded-xl border border-zinc-950/10 bg-white py-1 shadow-lg dark:border-white/10 dark:bg-zinc-900" id="company-autocomplete-listbox" role="listbox">
              {results.map((company, i) => <li aria-selected={i === active} className={`flex cursor-pointer items-center gap-3 px-3 py-2.5 ${i === active ? "bg-zinc-100 dark:bg-zinc-800" : "bg-transparent"}`} id={`company-autocomplete-option-${i}`} key={company.domain} onMouseDown={e => {
    e.preventDefault();
    select(company);
  }} onMouseEnter={() => setActive(i)} role="option">
                  <img alt="" className="h-7 w-7 rounded-md object-contain" height="28" loading="lazy" src={logoSrc(company.domain)} width="28" />
                  <span className="text-sm font-medium text-zinc-950 dark:text-white">
                    {company.name}
                  </span>
                  <span className="ml-auto text-xs text-zinc-400">
                    {company.domain}
                  </span>
                </li>)}
            </ul>}
          {open && query.trim() && results.length === 0 && <div className="absolute z-10 mt-2 w-full rounded-xl border border-zinc-950/10 bg-white px-3 py-2.5 text-sm text-zinc-400 shadow-lg dark:border-white/10 dark:bg-zinc-900" role="status">
              No companies found
            </div>}
        </div>
      </div>
    </div>;
};

The classic "which company?" field lets users type a few letters and pick their company from a dropdown of logos. One pick hands you a clean name and domain to store, instead of a hand-typed guess.

A user types `st`, sees Stripe with its logo at the top of the list, and selects it. Each suggestion comes from the [Search API](/docs/brand-search/introduction), which returns the name, domain, and a ready-to-use logo URL. If you're migrating from Clearbit's autocomplete, this is the replacement.

## Demo

Try typing `ni`, `s`, or `fig`:

<CompanyAutocompleteDemo />

The logos load live from `img.logo.dev`, but this demo filters a small local sample instead of calling the [Search API](/docs/brand-search/introduction); in production the suggestions arrive with the same shape and logos.

<Note>
  Using shadcn/ui? Install this whole pattern (combobox, debounced hook, and
  the server route) with one command: `npx shadcn@latest add
      https://www.logo.dev/r/brand-search.json`. See [shadcn/ui
  components](/docs/integrations/shadcn).
</Note>

## Build it with AI

<Prompt description="Want company search with logos in your app? This prompt gives your AI coding tool everything it needs to build it in your framework." actions={["copy", "cursor"]}>
  Build a company search autocomplete using Logo.dev.

  * Search runs through the Logo.dev Search API, called server-side: GET [https://api.logo.dev/search?q=:name](https://api.logo.dev/search?q=:name) with the header Authorization: Bearer LOGO\_DEV\_SECRET\_KEY. Put the call in a small server route and keep the secret key on the server; the browser fetches your route.
  * Each result has name, domain, and logo\_url fields. Render each suggestion's logo by appending format=webp\&retina=true\&size=64 to its logo\_url.
  * Fetch suggestions as the user types: debounce input by 200ms and abort stale requests so old results never overwrite fresh ones. Show at most 6 results.
  * Make it a real combobox: arrow keys move the highlight, Enter selects, Escape closes, with the ARIA roles and attributes screen readers expect. Give logo images fixed dimensions so the list never shifts.
  * Reference implementation: [https://www.logo.dev/docs/use-cases/company-autocomplete](https://www.logo.dev/docs/use-cases/company-autocomplete)
</Prompt>

## Code

This example is in React, but you can get the same result in any frontend framework: the pattern is one server route plus an input that fetches it. `api/company-search.ts` holds your secret key on the server, `CompanyAutocomplete.tsx` is the field, and the Usage tab shows the call site. Each result has the [Search API](/docs/brand-search/introduction) shape: `name`, `domain`, and `logo_url`.

<CodeGroup>
  ```ts api/company-search.ts theme={null}
  export async function GET(request: Request) {
    const q = new URL(request.url).searchParams.get("q")?.trim();
    if (!q) {
      return Response.json([]);
    }

    const res = await fetch(
      `https://api.logo.dev/search?q=${encodeURIComponent(q)}`,
      { headers: { Authorization: `Bearer ${process.env.LOGO_DEV_SECRET_KEY}` } }
    );
    if (!res.ok) {
      return Response.json([], { status: 502 });
    }

    // [{ name, domain, logo_url }]
    return Response.json(await res.json());
  }
  ```

  ```tsx CompanyAutocomplete.tsx expandable theme={null}
  import { useEffect, useRef, useState } from "react";

  type Company = { name: string; domain: string; logo_url: string };

  export function CompanyAutocomplete({
    onSelect,
  }: {
    onSelect?: (company: Company) => void;
  }) {
    const [query, setQuery] = useState("");
    const [results, setResults] = useState<Company[]>([]);
    const [open, setOpen] = useState(false);
    const [active, setActive] = useState(0);
    const [selected, setSelected] = useState<Company | null>(null);
    const [error, setError] = useState(false);
    const [loading, setLoading] = useState(false);
    const abortRef = useRef<AbortController | null>(null);

    // Debounced fetch; aborts the in-flight request when the query changes.
    useEffect(() => {
      const q = query.trim();
      if (!q || selected) {
        setResults([]);
        setError(false);
        setLoading(false);
        return;
      }
      setLoading(true);
      const timer = setTimeout(async () => {
        abortRef.current?.abort();
        const controller = new AbortController();
        abortRef.current = controller;
        try {
          const res = await fetch(
            `/api/company-search?q=${encodeURIComponent(q)}`,
            { signal: controller.signal }
          );
          if (!res.ok) {
            throw new Error("Search request failed");
          }
          setResults(((await res.json()) as Company[]).slice(0, 6));
          setActive(0);
          setError(false);
          setLoading(false);
        } catch (err) {
          // Ignore the abort fired by the next keystroke; surface anything else.
          if ((err as Error).name !== "AbortError") {
            setResults([]);
            setError(true);
            setLoading(false);
          }
        }
      }, 200);
      return () => clearTimeout(timer);
    }, [query, selected]);

    const select = (company: Company) => {
      setSelected(company);
      setQuery(company.name);
      setOpen(false);
      onSelect?.(company);
    };

    const onKeyDown = (e: React.KeyboardEvent) => {
      if (!(open && results.length)) {
        return;
      }
      if (e.key === "ArrowDown") {
        e.preventDefault();
        setActive((i) => (i + 1) % results.length);
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        setActive((i) => (i - 1 + results.length) % results.length);
      } else if (e.key === "Enter") {
        e.preventDefault();
        select(results[active]);
      } else if (e.key === "Escape") {
        setOpen(false);
      }
    };

    return (
      <div className="relative max-w-md">
        <input
          aria-activedescendant={
            open && results.length > 0 ? `company-option-${active}` : undefined
          }
          aria-autocomplete="list"
          aria-controls="company-listbox"
          aria-expanded={open && results.length > 0}
          autoComplete="off"
          className="w-full rounded-xl border border-zinc-950/10 bg-white py-3 px-4 text-base focus:outline-none focus:ring-2 focus:ring-zinc-950/20 dark:border-white/20 dark:bg-zinc-900 dark:text-white"
          onBlur={() => setOpen(false)}
          onChange={(e) => {
            setQuery(e.target.value);
            setSelected(null);
            setOpen(true);
          }}
          onFocus={() => setOpen(true)}
          onKeyDown={onKeyDown}
          placeholder="Type a company name…"
          role="combobox"
          type="text"
          value={query}
        />
        {open && results.length > 0 && (
          <ul
            className="absolute z-10 mt-2 w-full overflow-hidden rounded-xl border border-zinc-950/10 bg-white py-1 shadow-lg dark:border-white/10 dark:bg-zinc-900"
            id="company-listbox"
            role="listbox"
          >
            {results.map((company, i) => (
              <li
                aria-selected={i === active}
                className={`flex cursor-pointer items-center gap-3 px-3 py-2.5 ${
                  i === active ? "bg-zinc-100 dark:bg-zinc-800" : ""
                }`}
                id={`company-option-${i}`}
                key={company.domain}
                onMouseDown={(e) => {
                  e.preventDefault();
                  select(company);
                }}
                onMouseEnter={() => setActive(i)}
                role="option"
              >
                <img
                  alt=""
                  className="h-7 w-7 rounded-md object-contain"
                  height={28}
                  loading="lazy"
                  src={`${company.logo_url}&format=webp&retina=true&size=64`}
                  width={28}
                />
                <span className="text-sm font-medium">{company.name}</span>
                <span className="ml-auto text-xs text-zinc-400">
                  {company.domain}
                </span>
              </li>
            ))}
          </ul>
        )}
        {error && (
          <p className="mt-2 text-sm text-red-600 dark:text-red-400" role="alert">
            Search unavailable. Try again.
          </p>
        )}
        {open && !error && !loading && query.trim() && results.length === 0 && (
          <div
            className="absolute z-10 mt-2 w-full rounded-xl border border-zinc-950/10 bg-white px-3 py-2.5 text-sm text-zinc-400 shadow-lg dark:border-white/10 dark:bg-zinc-900"
            role="status"
          >
            No companies found
          </div>
        )}
      </div>
    );
  }
  ```

  ```tsx Usage theme={null}
  import { useState } from "react";
  import { CompanyAutocomplete } from "./CompanyAutocomplete";

  export function AttachCompany() {
    const [domain, setDomain] = useState<string | null>(null);
    return (
      <div>
        <CompanyAutocomplete onSelect={(company) => setDomain(company.domain)} />
        {domain && <p>Attached: {domain}</p>}
      </div>
    );
  }
  ```
</CodeGroup>

<Note>
  Your [secret key](/docs/platform/api-keys) lives in the server route, so the
  browser only ever talks to your own endpoint.
</Note>

## How it works

* **The Search API turns a few letters into companies.** A query to `api.logo.dev/search` returns the name, domain, and a ready-to-use `logo_url` for each match. See the [Search API](/docs/brand-search/introduction).
* **Each result ships its own logo URL.** Append [image parameters](/docs/logo-images/introduction#parameters) like `size` and `format` straight onto `logo_url` instead of doing a second lookup.
* **Your secret key stays on the server.** The browser calls your own route, and the route adds the `Authorization` header before forwarding the query. See [API keys](/docs/platform/api-keys).
* **The field behaves like a native picker.** Suggestions track your typing, the keyboard moves the highlight, and selecting fills the field. The component above carries the details.

## Make it your own

* **Return one exact match.** Pass `strategy=match` to the [Search API](/docs/brand-search/introduction) when you want the single best result instead of typeahead suggestions.
* **Store the domain with the record.** Feed it to the [CRM logos](/docs/use-cases/crm-logos) pattern so the logo follows the company through your whole app.

## Next steps

<CardGroup cols={2}>
  <Card title="Search API" icon="magnifying-glass" href="/docs/brand-search/introduction">
    Read the full parameters and response shape for `/search`.
  </Card>

  <Card title="Logo from a domain" icon="shield" href="/docs/use-cases/logo-from-domain">
    Harden the result logos for domains without one.
  </Card>
</CardGroup>
