Skip to main content
Beta FeatureThis feature is currently in beta, and needs to be imported from @servicetitan/anvil2/beta.While we hope to minimize breaking changes, they may occur due to feedback we receive or other improvements. These will always be documented in the changelog and communicated in Slack.Please reach out in the #ask-designsystem channel with any questions or feedback!

Overview

TypeaheadTextField extends TextField with a suggestion menu that opens as the user types. Provide a loadOptions callback that returns the matching options for the current input value; it may return an array synchronously or a Promise for remote data. Unlike SelectField, the input accepts any free-form text — suggestions assist entry rather than constrain it to a fixed set.Reach for TypeaheadTextField over SelectField when a suggestion needs to display more than what belongs in the field. On selection, only the option’s label is written to the input, while the full option — including its structured value — is delivered to onSelectOption. This keeps supporting detail shown in the menu out of the field, while letting that detail populate the rest of the form. For example, an address typeahead can render full formatted addresses in the menu, write only the street line to the field, and use onSelectOption to fill the city, state, and postal code fields.Because the field also accepts free-form text, set isHighlighted whenever its value comes from a selected suggestion so users can tell a confirmed choice apart from typed text. Track this alongside the value: set it true in onSelectOption and false in onChange. See Highlighting a selected value.Options are objects of type TypeaheadTextFieldOption:
type TypeaheadTextFieldOption<T = string> = {
  label: string;
  value: T;
};

Basic Usage

import { TypeaheadTextField } from "@servicetitan/anvil2/beta";

const FRUIT = [
  { label: "Apple", value: "apple" },
  { label: "Apricot", value: "apricot" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
  { label: "Grape", value: "grape" },
];

function App() {
  return (
    <TypeaheadTextField
      label="Fruit"
      description="Type to search for a fruit"
      placeholder="Start typing..."
      loadOptions={(search) =>
        FRUIT.filter((f) =>
          f.label.toLowerCase().includes(search.toLowerCase()),
        )
      }
    />
  );
}

export default App;

Common Examples

Synchronous options

Return an array from loadOptions to filter a static list on the client. Set debounceMs={0} when there is no network cost to debounce against.
import { TypeaheadTextField } from "@servicetitan/anvil2/beta";

const FRUIT = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
  { label: "Grape", value: "grape" },
  { label: "Mango", value: "mango" },
];

function App() {
  return (
    <TypeaheadTextField
      label="Fruit"
      placeholder="Type to filter fruit"
      debounceMs={0}
      loadOptions={(search) =>
        FRUIT.filter((f) =>
          f.label.toLowerCase().includes(search.toLowerCase()),
        )
      }
    />
  );
}

export default App;

Asynchronous options

Return a Promise to load suggestions from a remote source. A spinner shows while the request is in flight, and the field discards stale responses so the menu always reflects the latest input. Tune debounceMs to limit how often loadOptions is called.
import { TypeaheadTextField } from "@servicetitan/anvil2/beta";

async function searchFruit(search: string) {
  const res = await fetch(`/api/fruit?q=${encodeURIComponent(search)}`);
  const data = await res.json();
  return data.map((f) => ({ label: f.name, value: f.id }));
}

function App() {
  return (
    <TypeaheadTextField
      label="Fruit"
      description="Suggestions load as you type"
      placeholder="Start typing..."
      debounceMs={300}
      loadOptions={searchFruit}
    />
  );
}

export default App;

Controlled

The input value is uncontrolled by default. Pass value and onChange to control it, and use onSelectOption to react when a suggestion is chosen.
import { TypeaheadTextField } from "@servicetitan/anvil2/beta";
import { useState } from "react";

const FRUIT = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
];

function App() {
  const [value, setValue] = useState("");
  const [selected, setSelected] = useState(null);

  return (
    <TypeaheadTextField
      label="Fruit"
      placeholder="Type to filter"
      debounceMs={0}
      value={value}
      onChange={setValue}
      onSelectOption={(option) => setSelected(option.value)}
      loadOptions={(search) =>
        FRUIT.filter((f) =>
          f.label.toLowerCase().includes(search.toLowerCase()),
        )
      }
    />
  );
}

export default App;

Highlighting a selected value

Set isHighlighted while the field’s value comes from a selected suggestion to mark it as a confirmed choice. Set it true in onSelectOption and clear it in onChange, so the highlight turns off the moment the user edits the value away from the suggestion. This is the recommended pattern whenever selecting a suggestion carries meaning beyond the text in the field.
import { TypeaheadTextField } from "@servicetitan/anvil2/beta";
import { useState } from "react";

const FRUIT = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
];

function App() {
  const [value, setValue] = useState("");
  const [isHighlighted, setIsHighlighted] = useState(false);

  return (
    <TypeaheadTextField
      label="Fruit"
      placeholder="Type to filter"
      debounceMs={0}
      value={value}
      isHighlighted={isHighlighted}
      onChange={(next) => {
        setValue(next);
        setIsHighlighted(false);
      }}
      onSelectOption={() => setIsHighlighted(true)}
      loadOptions={(search) =>
        FRUIT.filter((f) =>
          f.label.toLowerCase().includes(search.toLowerCase()),
        )
      }
    />
  );
}

export default App;

Open on empty focus

By default the menu opens only when the input holds a value. Pass openOnEmptyFocus to also open it on focus or click while the input is empty, which suits “show all” menus that don’t require pre-typing.
import { TypeaheadTextField } from "@servicetitan/anvil2/beta";

const FRUIT = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
];

function App() {
  return (
    <TypeaheadTextField
      label="Fruit"
      description="Menu opens on focus even when empty"
      placeholder="Click to see options"
      debounceMs={0}
      openOnEmptyFocus
      loadOptions={(search) =>
        FRUIT.filter((f) =>
          f.label.toLowerCase().includes(search.toLowerCase()),
        )
      }
    />
  );
}

export default App;

Custom option rendering

Pass renderOption to render richer content for each suggestion row. The returned node replaces the default label text; loadOptions still drives which options appear.
import { TypeaheadTextField, Flex, Text } from "@servicetitan/anvil2/beta";

const FRUIT = [
  { label: "Apple", value: { id: "apple", kcal: 95 } },
  { label: "Banana", value: { id: "banana", kcal: 105 } },
  { label: "Cherry", value: { id: "cherry", kcal: 50 } },
];

function App() {
  return (
    <TypeaheadTextField
      label="Fruit"
      placeholder="Type to filter"
      debounceMs={0}
      loadOptions={(search) =>
        FRUIT.filter((f) =>
          f.label.toLowerCase().includes(search.toLowerCase()),
        )
      }
      renderOption={(option) => (
        <Flex justify="space-between" gap="4">
          <Text>{option.label}</Text>
          <Text subdued>{option.value.kcal} kcal</Text>
        </Flex>
      )}
    />
  );
}

export default App;

React Accessibility

Provide a label so screen readers announce the field’s purpose. The component wires up the combobox and listbox ARIA roles, expanded state, and the relationship between the input and the suggestion menu.For more guidance on form field labels and context, see input field context association best practices.
Last modified on June 12, 2026