> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Typeahead Text Field – Code

> TypeaheadTextField is a text input that suggests matching options as the user types, with support for synchronous and asynchronous data loading.

<Note>
  **Beta Feature**

  This 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](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) channel with any questions or feedback!
</Note>

<Tabs>
  <Tab title="Implementation">
    ## 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](#highlighting-a-selected-value).

    Options are objects of type `TypeaheadTextFieldOption`:

    ```tsx theme={null}
    type TypeaheadTextFieldOption<T = string> = {
      label: string;
      value: T;
    };
    ```

    ## Basic Usage

    ```tsx lines expandable theme={null}
    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.

    ```tsx lines expandable theme={null}
    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.

    ```tsx lines expandable theme={null}
    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.

    ```tsx lines expandable theme={null}
    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.

    ```tsx lines expandable theme={null}
    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.

    ```tsx lines expandable theme={null}
    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.

    ```tsx lines expandable theme={null}
    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](/docs/accessibility/labels-and-ctas#input-field-context-association).
  </Tab>

  <Tab title="TypeaheadTextField Props">
    ```tsx theme={null}
    <TypeaheadTextField
      label="Fruit"
      placeholder="Start typing..."
      debounceMs={300}
      openOnFocus
      loadOptions={(search) => filterFruit(search)}
      onChange={(value) => console.log(value)}
      onSelectOption={(option) => console.log(option.value)}
    />
    ```

    ## `TypeaheadTextField` Props

    `TypeaheadTextField` accepts all [`TextField`](/docs/web/components/text-field/code) props except `value`, `defaultValue`, `onChange`, and `suffix` (including `label`, `description`, `size`, `disabled`, `readOnly`, `error`, `loading`, and `isHighlighted`), as well as the following:

    <ParamField path="loadOptions" type="(search: string) => TypeaheadTextFieldOption<T>[] | Promise<TypeaheadTextFieldOption<T>[]>" required>
      Returns the suggestions for the current input value. May return an array synchronously or a `Promise` for asynchronous data.
    </ParamField>

    <ParamField path="debounceMs" type="number" default="300">
      Delay in milliseconds between an input change and the `loadOptions` call.
    </ParamField>

    <ParamField path="defaultValue" type="string">
      Initial input value for uncontrolled usage.
    </ParamField>

    <ParamField path="loadingLabel" type="string" default={`"Loading..."`}>
      Message shown while suggestions load and no options are available yet.
    </ParamField>

    <ParamField path="noSuggestionsLabel" type="string" default={`"No suggestions found"`}>
      Message shown when there are no suggestions to render.
    </ParamField>

    <ParamField path="onChange" type="(value: string) => void">
      Fires whenever the input value changes, from typing, selection, or clearing.
    </ParamField>

    <ParamField path="onSelectOption" type="(option: TypeaheadTextFieldOption<T>) => void">
      Fires when a suggestion is selected from the menu.
    </ParamField>

    <ParamField path="openOnEmptyFocus" type="boolean" default="false">
      Opens the menu on focus or click even when the input is empty.
    </ParamField>

    <ParamField path="openOnFocus" type="boolean" default="true">
      Opens the menu when the input is focused and holds a non-empty value.
    </ParamField>

    <ParamField path="renderOption" type="(option: TypeaheadTextFieldOption<T>) => ReactNode">
      Custom renderer for the content of an option row.
    </ParamField>

    <ParamField path="value" type="string">
      Controlled input value.
    </ParamField>

    ## `TypeaheadTextFieldOption` Type

    <ParamField path="label" type="string" required>
      Text shown in the suggestion row and written to the input on selection.
    </ParamField>

    <ParamField path="value" type="T" required>
      The value passed to `onSelectOption` when the option is chosen. Defaults to `string`.
    </ParamField>
  </Tab>
</Tabs>
