# Combobox

URL: https://ark-ui.com/docs/components/combobox
Source: https://raw.githubusercontent.com/chakra-ui/ark/refs/heads/main/website/src/content/pages/components/combobox.mdx

A single input field that combines the functionality of a select and input.

---

<ComponentPreview id="Combobox" />

## Anatomy

<Anatomy id="combobox" />

```tsx
<Combobox.Root>
  <Combobox.Label />
  <Combobox.Control>
    <Combobox.Input />
    <Combobox.Trigger />
    <Combobox.ClearTrigger />
  </Combobox.Control>
  <Combobox.Positioner>
    <Combobox.Content>
      <Combobox.ItemGroup>
        <Combobox.ItemGroupLabel />
        <Combobox.Item>
          <Combobox.ItemText />
          <Combobox.ItemIndicator />
        </Combobox.Item>
      </Combobox.ItemGroup>
    </Combobox.Content>
  </Combobox.Positioner>
</Combobox.Root>
```

## Examples

**Example: basic**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component Basic() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems: [
        { label: 'Apple', value: 'apple' },
        { label: 'Banana', value: 'banana' },
        { label: 'Orange', value: 'orange' },
        { label: 'Mango', value: 'mango' },
        { label: 'Pineapple', value: 'pineapple' },
        { label: 'Strawberry', value: 'strawberry' },
      ],
      filter: @filterApi.contains,
    },
  );

  <Combobox.Root
    class={styles.Root}
    {collection}
    onInputValueChange={(details) => {
      filter(details.inputValue);
    }}
  >
    <Combobox.Label class={styles.Label}>{'Favorite Fruit'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Apple" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Auto Highlight

Automatically highlight the first matching item as the user types by setting `inputBehavior="autohighlight"`.

**Example: auto-highlight**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component AutoHighlight() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems: [
        { label: 'Engineering', value: 'engineering' },
        { label: 'Marketing', value: 'marketing' },
        { label: 'Sales', value: 'sales' },
        { label: 'Finance', value: 'finance' },
        { label: 'Human Resources', value: 'hr' },
        { label: 'Operations', value: 'operations' },
        { label: 'Product', value: 'product' },
        { label: 'Customer Success', value: 'customer-success' },
        { label: 'Legal', value: 'legal' },
        { label: 'Information Technology', value: 'information-technology' },
        { label: 'Design', value: 'design' },
      ],
      filter: @filterApi.contains,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root
    class={styles.Root}
    {collection}
    onInputValueChange={handleInputChange}
    inputBehavior="autohighlight"
  >
    <Combobox.Label class={styles.Label}>{'Department'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Engineering" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          <Combobox.Empty class={styles.Item}>{'No results found'}</Combobox.Empty>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Inline Autocomplete

Complete the input value with the first matching item by setting `inputBehavior="autocomplete"`. Use with `startsWith`
filter for best results.

**Example: inline-autocomplete**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component InlineAutocomplete() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems: [
        { label: 'Whale', value: 'whale' },
        { label: 'Dolphin', value: 'dolphin' },
        { label: 'Shark', value: 'shark' },
        { label: 'Octopus', value: 'octopus' },
        { label: 'Jellyfish', value: 'jellyfish' },
        { label: 'Seahorse', value: 'seahorse' },
      ],
      filter: @filterApi.startsWith,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root
    class={styles.Root}
    {collection}
    onInputValueChange={handleInputChange}
    inputBehavior="autocomplete"
  >
    <Combobox.Label class={styles.Label}>{'Sea Creature'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Dolphin" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          <Combobox.Empty class={styles.Item}>{'No results found'}</Combobox.Empty>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Grouping

To group related combobox items, use the `groupBy` prop on the collection and `collection.group()` to iterate the
groups.

**Example: grouping**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component Grouping() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems,
      filter: @filterApi.contains,
      groupBy: (item) => item.continent,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
    <Combobox.Label class={styles.Label}>{'Country'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Canada" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const [continent, group] of @collection.group(); key continent) {
            <Combobox.ItemGroup class={styles.ItemGroup}>
              <Combobox.ItemGroupLabel class={styles.ItemGroupLabel}>
                {continent}
              </Combobox.ItemGroupLabel>
              for (const item of group; key item.value) {
                <Combobox.Item class={styles.Item} {item}>
                  <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
                  <Combobox.ItemIndicator class={styles.ItemIndicator}>
                    <Check />
                  </Combobox.ItemIndicator>
                </Combobox.Item>
              }
            </Combobox.ItemGroup>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

const initialItems = [
  { label: 'Canada', value: 'ca', continent: 'North America' },
  { label: 'United States', value: 'us', continent: 'North America' },
  { label: 'Mexico', value: 'mx', continent: 'North America' },
  { label: 'United Kingdom', value: 'uk', continent: 'Europe' },
  { label: 'Germany', value: 'de', continent: 'Europe' },
  { label: 'France', value: 'fr', continent: 'Europe' },
  { label: 'Japan', value: 'jp', continent: 'Asia' },
  { label: 'South Korea', value: 'kr', continent: 'Asia' },
  { label: 'China', value: 'cn', continent: 'Asia' },
];

```

### Field

The `Field` component helps manage form-related state and accessibility attributes of a combobox. It includes handling
ARIA labels, helper text, and error text to ensure proper accessibility.

**Example: with-field**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { Field } from 'ark-ripple/field';
import { useFilter } from 'ark-ripple/locale';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import field from 'styles/field.module.css';
import styles from 'styles/combobox.module.css';

const initialItems = [
  { label: 'Engineering', value: 'engineering' },
  { label: 'Design', value: 'design' },
  { label: 'Marketing', value: 'marketing' },
  { label: 'Sales', value: 'sales' },
  { label: 'Human Resources', value: 'hr' },
  { label: 'Finance', value: 'finance' },
];

export component WithField() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems,
      filter: @filterApi.contains,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Field.Root class={field.Root}>
    <Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
      <Combobox.Label class={styles.Label}>{'Department'}</Combobox.Label>
      <Combobox.Control class={styles.Control}>
        <Combobox.Input class={styles.Input} placeholder="e.g. Engineering" />
        <div class={styles.Indicators}>
          <Combobox.ClearTrigger class={styles.ClearTrigger}>
            <X />
          </Combobox.ClearTrigger>
          <Combobox.Trigger class={styles.Trigger}>
            <ChevronsUpDown />
          </Combobox.Trigger>
        </div>
      </Combobox.Control>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Combobox.Root>
    <Field.HelperText class={field.HelperText}>{'Select your primary department'}</Field.HelperText>
    <Field.ErrorText class={field.ErrorText}>{'Department is required'}</Field.ErrorText>
  </Field.Root>
}

```

### Context

Access the combobox's state with `Combobox.Context` or the `useComboboxContext` hook—useful for displaying the selected
value or building custom UI.

**Example: context**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component Context() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems: [
        { label: 'Small', value: 'sm' },
        { label: 'Medium', value: 'md' },
        { label: 'Large', value: 'lg' },
        { label: 'Extra Large', value: 'xl' },
      ],
      filter: @filterApi.contains,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
    <Combobox.Context>
      component children({ context }) {
        <p>
          {'Selected: '}
          {@context.valueAsString || 'None'}
        </p>
      }
    </Combobox.Context>
    <Combobox.Label class={styles.Label}>{'Size'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Medium" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Root Provider

An alternative way to control the combobox is to use the `RootProvider` component and the `useCombobox` hook. This way
you can access the state and methods from outside the component.

**Example: root-provider**

```ripple
import { Combobox, useCombobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/combobox.module.css';

const initialItems = [
  { label: 'Designer', value: 'designer' },
  { label: 'Developer', value: 'developer' },
  { label: 'Product Manager', value: 'pm' },
  { label: 'Data Scientist', value: 'data-scientist' },
  { label: 'DevOps Engineer', value: 'devops' },
  { label: 'Marketing Lead', value: 'marketing' },
];

export component RootProvider() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems,
      filter: @filterApi.contains,
    },
  );

  const combobox = useCombobox(
    {
      collection,
      onInputValueChange: (details) => {
        filter(details.inputValue);
      },
    },
  );

  <div>
    <button
      class={button.Root}
      type="button"
      onClick={() => {
        @combobox.focus();
      }}
    >
      {'Focus'}
    </button>

    <Combobox.RootProvider class={styles.Root} value={combobox}>
      <Combobox.Label class={styles.Label}>{'Job Title'}</Combobox.Label>
      <Combobox.Control class={styles.Control}>
        <Combobox.Input class={styles.Input} placeholder="e.g. Designer" />
        <div class={styles.Indicators}>
          <Combobox.ClearTrigger class={styles.ClearTrigger}>
            <X />
          </Combobox.ClearTrigger>
          <Combobox.Trigger class={styles.Trigger}>
            <ChevronsUpDown />
          </Combobox.Trigger>
        </div>
      </Combobox.Control>
      <Portal>
        <Combobox.Positioner>
          <Combobox.Content class={styles.Content}>
            for (const item of @collection.items; key item.value) {
              <Combobox.Item class={styles.Item} {item}>
                <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
                <Combobox.ItemIndicator class={styles.ItemIndicator}>
                  <Check />
                </Combobox.ItemIndicator>
              </Combobox.Item>
            }
          </Combobox.Content>
        </Combobox.Positioner>
      </Portal>
    </Combobox.RootProvider>
  </div>
}

```

### Links

Use the `asChild` prop to render the combobox items as links.

**Example: links**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component Links() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems,
      filter: @filterApi.contains,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root
    class={styles.Root}
    {collection}
    onInputValueChange={handleInputChange}
    selectionBehavior="preserve"
  >
    <Combobox.Label class={styles.Label}>{'Developer Resources'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. GitHub" />
      <div class={styles.Indicators}>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              component asChild({ propsFn }) {
                <a {...propsFn({ href: item.href })}>
                  <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
                  <Combobox.ItemIndicator class={styles.ItemIndicator}>
                    <Check />
                  </Combobox.ItemIndicator>
                </a>
              }
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

const initialItems = [
  { label: 'GitHub', href: 'https://github.com', value: 'github' },
  { label: 'Stack Overflow', href: 'https://stackoverflow.com', value: 'stackoverflow' },
  { label: 'MDN Web Docs', href: 'https://developer.mozilla.org', value: 'mdn' },
  { label: 'npm', href: 'https://www.npmjs.com', value: 'npm' },
  { label: 'TypeScript', href: 'https://www.typescriptlang.org', value: 'typescript' },
  { label: 'React', href: 'https://react.dev', value: 'react' },
];

```

### Rehydrate

When a combobox has a `defaultValue` or `value` but the `collection` is not loaded yet, you can rehydrate the value to
populate the input.

**Example: rehydrate-value**

```ripple
import { Combobox, useCombobox, useComboboxContext, useListCollection } from 'ark-ripple/combobox';
import { Portal } from 'ark-ripple/portal';
import { Check } from 'lucide-ripple';
import { track, effect } from 'ripple';
import styles from 'styles/combobox.module.css';
import { useAsync } from './use-async';

export component RehydrateValue() {
  let inputValue = track('');

  let { collection, set } = useListCollection<Character>(
    {
      initialItems: [],
      itemToString: (item) => item.name,
      itemToValue: (item) => item.name,
    },
  );

  const combobox = useCombobox(
    {
      collection,
      defaultValue: ['C-3PO'],
      placeholder: 'Example: Dexter',
      inputValue,
      onInputValueChange: (e) => {
        @inputValue = e.inputValue;
      },
    },
  );

  let state = useAsync(async (signal) => {
    const response = await fetch(`https://swapi.py4e.com/api/people/?search=${@inputValue}`, {
      signal,
    });
    const data = await response.json();
    set(data.results);
  });

  effect(() => {
    void @inputValue;
    void state.load();
  });

  let hydrated = false;
  effect(() => {
    if (@combobox.value.length && @combobox.collection.size && !hydrated) {
      @combobox.syncSelectedItems();
      hydrated = true;
    }
  });

  <Combobox.RootProvider class={styles.Root} value={combobox}>
    <Combobox.Label class={styles.Label}>{'Search Star Wars Characters'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Luke" />
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          if (state.@loading) {
            <span style={{ padding: '0.5rem' }}>{'Loading...'}</span>
          } else if (state.@error) {
            <span style={{ padding: '0.5rem' }}>{state.@error?.message}</span>
          } else {
            for (const item of @collection.items; key item.name) {
              <Combobox.Item class={styles.Item} {item}>
                <Combobox.ItemText class={styles.ItemText}>
                  {item.name}
                  {' - '}
                  {item.height}
                  {'cm / '}
                  {item.mass}
                  {'kg'}
                </Combobox.ItemText>
                <Combobox.ItemIndicator class={styles.ItemIndicator}>
                  <Check />
                </Combobox.ItemIndicator>
              </Combobox.Item>
            }
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.RootProvider>
}

interface Character {
  name: string;
  height: string;
  mass: string;
  created: string;
  edited: string;
  url: string;
}

```

### Highlight Text

Highlight the matching search text in combobox items based on the user's input.

**Example: highlight-matching-text**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { Highlight } from 'ark-ripple/highlight';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { ChevronsUpDown, X } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component HighlightMatchingText() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems: [
        { label: 'John Smith', value: 'john-smith' },
        { label: 'Jane Doe', value: 'jane-doe' },
        { label: 'Bob Johnson', value: 'bob-johnson' },
        { label: 'Alice Williams', value: 'alice-williams' },
        { label: 'Charlie Brown', value: 'charlie-brown' },
        { label: 'Diana Ross', value: 'diana-ross' },
      ],
      filter: @filterApi.contains,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
    <Combobox.Label class={styles.Label}>{'Assignee'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. John Smith" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>
                <Combobox.Context>
                  component children({ context }) {
                    <Highlight text={item.label} query={@context.inputValue} ignoreCase />
                  }
                </Combobox.Context>
              </Combobox.ItemText>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Dynamic

Generate combobox items dynamically based on user input. This is useful for creating suggestions or autocomplete
functionality.

**Example: dynamic**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

const suggestList = ['gmail.com', 'yahoo.com', 'ark-ui.rip'];

export component Dynamic() {
  let { collection, set } = useListCollection<string>(
    {
      initialItems: [],
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    if (details.reason === 'input-change') {
      const items = suggestList.map((item) => `${details.inputValue}@${item}`);
      set(items);
    }
  };

  <Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
    <Combobox.Label class={styles.Label}>{'Email'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. john" />
      <div class={styles.Indicators}>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>{item}</Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Creatable

Allow users to create new options when their search doesn't match any existing items. This is useful for tags,
categories, or other custom values.

**Example: creatable**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

interface Item {
  label: string;
  value: string;
  __new__?: boolean | undefined;
}

const NEW_OPTION_VALUE = '[[new]]';
const createNewOption = (value: string): Item => ({ label: value, value: NEW_OPTION_VALUE });
const isNewOptionValue = (value: string) => value === NEW_OPTION_VALUE;
const replaceNewOptionValue = (values: string[], value: string) => values.map(
  (v) => (v === NEW_OPTION_VALUE ? value : v),
);
const getNewOptionData = (inputValue: string): Item => ({
  label: inputValue,
  value: inputValue,
  __new__: true,
});

export component Creatable() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter, upsert, remove, update } = useListCollection<Item>(
    {
      initialItems: [
        { label: 'Bug', value: 'bug' },
        { label: 'Feature', value: 'feature' },
        { label: 'Enhancement', value: 'enhancement' },
        { label: 'Documentation', value: 'docs' },
      ],
      filter: @filterApi.contains,
    },
  );

  const isValidNewOption = (inputValue: string) => {
    const exactOptionMatch =
      @collection.filter((item) => item.toLowerCase() === inputValue.toLowerCase()).size > 0;
    return !exactOptionMatch && inputValue.trim().length > 0;
  };

  let selectedValue = track<string[]>([]);
  let inputValue = track('');

  <Combobox.Root
    class={styles.Root}
    {collection}
    value={selectedValue}
    allowCustomValue
    onInputValueChange={(details) => {
      if (details.reason === 'input-change' || details.reason === 'item-select') {
        if (isValidNewOption(details.inputValue)) {
          upsert(NEW_OPTION_VALUE, createNewOption(details.inputValue));
        } else if (details.inputValue.trim().length === 0) {
          remove(NEW_OPTION_VALUE);
        }
        filter(details.inputValue);
      }
      @inputValue = details.inputValue;
    }}
    onOpenChange={(details) => {
      if (details.reason === 'trigger-click') {
        filter('');
      }
    }}
    onValueChange={(details) => {
      @selectedValue = replaceNewOptionValue(details.value, @inputValue);
      if (details.value.includes(NEW_OPTION_VALUE)) {
        update(NEW_OPTION_VALUE, getNewOptionData(@inputValue));
      }
    }}
  >
    <Combobox.Label class={styles.Label}>{'Label'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Bug" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              if (isNewOptionValue(item.value)) {
                <Combobox.ItemText class={styles.ItemText}>
                  {'+ Create "'}
                  {item.label}
                  {'"'}
                </Combobox.ItemText>
              } else {
                <Combobox.ItemText class={styles.ItemText}>
                  {item.label}
                  {item.__new__ ? ' (new)' : ''}
                </Combobox.ItemText>
              }
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Multiple Selection

Enable multiple selection by setting the `multiple` prop. Selected items can be displayed as tags above the input.

**Example: multiple**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component Multiple() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter, remove } = useListCollection(
    {
      initialItems: [
        { label: 'JavaScript', value: 'js' },
        { label: 'TypeScript', value: 'ts' },
        { label: 'Python', value: 'python' },
        { label: 'Go', value: 'go' },
        { label: 'Rust', value: 'rust' },
        { label: 'Java', value: 'java' },
      ],
      filter: @filterApi.contains,
    },
  );

  <Combobox.Root
    class={styles.Root}
    {collection}
    onInputValueChange={(details) => {
      filter(details.inputValue);
    }}
    onValueChange={(details) => {
      remove(...details.value);
    }}
    multiple
  >
    <Combobox.Label class={styles.Label}>{'Skills'}</Combobox.Label>
    <Combobox.Context>
      component children({ context }) {
        <div class={styles.Tags}>
          if (@context.selectedItems.length === 0) {
            <span class={styles.TagPlaceholder}>{'None selected'}</span>
          }
          for (const item of @context.selectedItems; key item.value) {
            <span class={styles.Tag}>{item.label}</span>
          }
        </div>
      }
    </Combobox.Context>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. JavaScript" />
      <div class={styles.Indicators}>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          <Combobox.Empty class={styles.Item}>{'No skills found'}</Combobox.Empty>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Async Search

Load options asynchronously based on user input using the `useAsyncList` hook. This is useful for searching large
datasets or fetching data from an API.

**Example: async-search**

```ripple
import { useAsyncList } from 'ark-ripple/collection';
import { Combobox, createListCollection } from 'ark-ripple/combobox';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, Loader, X } from 'lucide-ripple';
import { track } from 'ripple';
import styles from 'styles/combobox.module.css';

interface Movie {
  id: string;
  title: string;
  year: number;
  director: string;
  genre: string;
}

export component AsyncSearch() {
  const list = useAsyncList<Movie>(
    {
      async load({ filterText, signal }: { filterText?: string; signal?: AbortSignal }) {
        if (!filterText) return { items: [] };

        await new Promise((resolve) => setTimeout(resolve, 300));

        if (signal?.aborted) return { items: [] };

        const items = allMovies.filter(
          (movie) => movie.title.toLowerCase().includes(filterText.toLowerCase()) ||
            movie.director.toLowerCase().includes(filterText.toLowerCase()) ||
            movie.genre.toLowerCase().includes(filterText.toLowerCase()),
        );

        return { items };
      },
    },
  );

  const collection = track(
    () => createListCollection(
      {
        items: @list.items,
        itemToString: (item) => item.title,
        itemToValue: (item) => item.id,
      },
    ),
  );

  <Combobox.Root
    class={styles.Root}
    {collection}
    onInputValueChange={(details) => {
      if (details.reason === 'input-change') {
        @list.setFilterText(details.inputValue);
      }
    }}
  >
    <Combobox.Label class={styles.Label}>{'Movie'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Inception" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          if (@list.loading) {
            <div class={styles.Status}>
              <Loader class={styles.Spinner} />
              <span>{'Searching...'}</span>
            </div>
          } else if (@list.error) {
            <div class={styles.Status}>{@list.error.message}</div>
          } else if (@list.items.length === 0) {
            <div class={styles.Status}>
              {@list.filterText ? 'No results found' : 'Start typing to search movies...'}
            </div>
          } else {
            for (const movie of @list.items; key movie.id) {
              <Combobox.Item class={styles.Item} item={movie}>
                <Combobox.ItemText class={styles.ItemText}>
                  <span class={styles.ItemTitle}>{movie.title}</span>
                  <span class={styles.ItemSubtitle}>
                    {movie.year}
                    {' · '}
                    {movie.director}
                  </span>
                </Combobox.ItemText>
                <Combobox.ItemIndicator class={styles.ItemIndicator}>
                  <Check />
                </Combobox.ItemIndicator>
              </Combobox.Item>
            }
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

const allMovies: Movie[] = [
  {
    id: 'inception',
    title: 'Inception',
    year: 2010,
    director: 'Christopher Nolan',
    genre: 'Sci-Fi',
  },
  {
    id: 'the-dark-knight',
    title: 'The Dark Knight',
    year: 2008,
    director: 'Christopher Nolan',
    genre: 'Action',
  },
  {
    id: 'pulp-fiction',
    title: 'Pulp Fiction',
    year: 1994,
    director: 'Quentin Tarantino',
    genre: 'Crime',
  },
  {
    id: 'the-godfather',
    title: 'The Godfather',
    year: 1972,
    director: 'Francis Ford Coppola',
    genre: 'Crime',
  },
  {
    id: 'forrest-gump',
    title: 'Forrest Gump',
    year: 1994,
    director: 'Robert Zemeckis',
    genre: 'Drama',
  },
  {
    id: 'the-matrix',
    title: 'The Matrix',
    year: 1999,
    director: 'The Wachowskis',
    genre: 'Sci-Fi',
  },
  {
    id: 'interstellar',
    title: 'Interstellar',
    year: 2014,
    director: 'Christopher Nolan',
    genre: 'Sci-Fi',
  },
  { id: 'parasite', title: 'Parasite', year: 2019, director: 'Bong Joon-ho', genre: 'Thriller' },
  {
    id: 'the-shawshank-redemption',
    title: 'The Shawshank Redemption',
    year: 1994,
    director: 'Frank Darabont',
    genre: 'Drama',
  },
  { id: 'fight-club', title: 'Fight Club', year: 1999, director: 'David Fincher', genre: 'Drama' },
  {
    id: 'goodfellas',
    title: 'Goodfellas',
    year: 1990,
    director: 'Martin Scorsese',
    genre: 'Crime',
  },
  {
    id: 'the-silence-of-the-lambs',
    title: 'The Silence of the Lambs',
    year: 1991,
    director: 'Jonathan Demme',
    genre: 'Thriller',
  },
];

```

### Virtualized

For very large lists, use virtualization with `@tanstack/virtual` to render only the visible items. Pass the
`scrollToIndexFn` prop to enable keyboard navigation within the virtualized list.

**Example: virtualized**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';
import { createVirtualizer } from '../../../utils/use-virtualizer.ripple';
import { flushSync, track } from 'ripple';

export component Virtualized() {
  let contentRef = track<HTMLDivElement | null>(null);

  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter, reset } = useListCollection(
    {
      initialItems: countries,
      filter: @filterApi.startsWith,
    },
  );

  const virtualizer = createVirtualizer(
    {
      get count() {
        return @collection.size;
      },
      getScrollElement: () => @contentRef,
      estimateSize: () => 32,
      overscan: 10,
    },
  );

  const handleScrollToIndex: Combobox.RootProps<Country>['scrollToIndexFn'] = (details) => {
    flushSync(() => {
      virtualizer.scrollToIndex(details.index, { align: 'center', behavior: 'auto' });
    });
  };

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root
    class={styles.Root}
    {collection}
    onInputValueChange={handleInputChange}
    scrollToIndexFn={handleScrollToIndex}
  >
    <Combobox.Label class={styles.Label}>{'Country'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Germany" />
      <div class={styles.Indicators}>
        <Combobox.Trigger class={styles.Trigger} onClick={reset}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          <div
            {ref (el: HTMLDivElement) => {
              @contentRef = el;
              return () => {
                @contentRef = null;
              };
            }}
            class={styles.Scroller}
          >
            <div
              style={{
                height: `${virtualizer.getTotalSize()}px`,
                width: '100%',
                position: 'relative',
              }}
            >
              for (const virtualItem of virtualizer.getVirtualItems(); key ((@collection.items[virtualItem.index])).value) {
                let item = @collection.items[virtualItem.index];
                <Combobox.Item
                  class={styles.Item}
                  {item}
                  aria-setsize={@collection.size}
                  aria-posinset={virtualItem.index + 1}
                  style={{
                    position: 'absolute',
                    top: '0',
                    left: '0',
                    width: '100%',
                    height: `${virtualItem.size}px`,
                    transform: `translateY(${virtualItem.start}px)`,
                  }}
                >
                  <Combobox.ItemText class={styles.ItemText}>
                    <span aria-hidden style={{ marginRight: '8px' }}>{item.emoji}</span>
                    {item.label}
                  </Combobox.ItemText>
                  <Combobox.ItemIndicator class={styles.ItemIndicator}>
                    <Check />
                  </Combobox.ItemIndicator>
                </Combobox.Item>
              }
            </div>
          </div>
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

interface Country {
  value: string;
  label: string;
  emoji: string;
}

const countries: Country[] = [
  { value: 'AD', label: 'Andorra', emoji: '🇦🇩' },
  { value: 'AE', label: 'United Arab Emirates', emoji: '🇦🇪' },
  { value: 'AF', label: 'Afghanistan', emoji: '🇦🇫' },
  { value: 'AG', label: 'Antigua and Barbuda', emoji: '🇦🇬' },
  { value: 'AI', label: 'Anguilla', emoji: '🇦🇮' },
  { value: 'AL', label: 'Albania', emoji: '🇦🇱' },
  { value: 'AM', label: 'Armenia', emoji: '🇦🇲' },
  { value: 'AO', label: 'Angola', emoji: '🇦🇴' },
  { value: 'AQ', label: 'Antarctica', emoji: '🇦🇶' },
  { value: 'AR', label: 'Argentina', emoji: '🇦🇷' },
  { value: 'AS', label: 'American Samoa', emoji: '🇦🇸' },
  { value: 'AT', label: 'Austria', emoji: '🇦🇹' },
  { value: 'AU', label: 'Australia', emoji: '🇦🇺' },
  { value: 'AW', label: 'Aruba', emoji: '🇦🇼' },
  { value: 'AX', label: 'Åland Islands', emoji: '🇦🇽' },
  { value: 'AZ', label: 'Azerbaijan', emoji: '🇦🇿' },
  { value: 'BA', label: 'Bosnia and Herzegovina', emoji: '🇧🇦' },
  { value: 'BB', label: 'Barbados', emoji: '🇧🇧' },
  { value: 'BD', label: 'Bangladesh', emoji: '🇧🇩' },
  { value: 'BE', label: 'Belgium', emoji: '🇧🇪' },
  { value: 'BF', label: 'Burkina Faso', emoji: '🇧🇫' },
  { value: 'BG', label: 'Bulgaria', emoji: '🇧🇬' },
  { value: 'BH', label: 'Bahrain', emoji: '🇧🇭' },
  { value: 'BI', label: 'Burundi', emoji: '🇧🇮' },
  { value: 'BJ', label: 'Benin', emoji: '🇧🇯' },
  { value: 'BL', label: 'Saint Barthélemy', emoji: '🇧🇱' },
  { value: 'BM', label: 'Bermuda', emoji: '🇧🇲' },
  { value: 'BN', label: 'Brunei', emoji: '🇧🇳' },
  { value: 'BO', label: 'Bolivia', emoji: '🇧🇴' },
  { value: 'BR', label: 'Brazil', emoji: '🇧🇷' },
  { value: 'BS', label: 'Bahamas', emoji: '🇧🇸' },
  { value: 'BT', label: 'Bhutan', emoji: '🇧🇹' },
  { value: 'BW', label: 'Botswana', emoji: '🇧🇼' },
  { value: 'BY', label: 'Belarus', emoji: '🇧🇾' },
  { value: 'BZ', label: 'Belize', emoji: '🇧🇿' },
  { value: 'CA', label: 'Canada', emoji: '🇨🇦' },
  { value: 'CD', label: 'Congo', emoji: '🇨🇩' },
  { value: 'CF', label: 'Central African Republic', emoji: '🇨🇫' },
  { value: 'CH', label: 'Switzerland', emoji: '🇨🇭' },
  { value: 'CI', label: 'Côte d\'Ivoire', emoji: '🇨🇮' },
  { value: 'CK', label: 'Cook Islands', emoji: '🇨🇰' },
  { value: 'CL', label: 'Chile', emoji: '🇨🇱' },
  { value: 'CM', label: 'Cameroon', emoji: '🇨🇲' },
  { value: 'CN', label: 'China', emoji: '🇨🇳' },
  { value: 'CO', label: 'Colombia', emoji: '🇨🇴' },
  { value: 'CR', label: 'Costa Rica', emoji: '🇨🇷' },
  { value: 'CU', label: 'Cuba', emoji: '🇨🇺' },
  { value: 'CV', label: 'Cabo Verde', emoji: '🇨🇻' },
  { value: 'CY', label: 'Cyprus', emoji: '🇨🇾' },
  { value: 'CZ', label: 'Czech Republic', emoji: '🇨🇿' },
  { value: 'DE', label: 'Germany', emoji: '🇩🇪' },
  { value: 'DJ', label: 'Djibouti', emoji: '🇩🇯' },
  { value: 'DK', label: 'Denmark', emoji: '🇩🇰' },
  { value: 'DM', label: 'Dominica', emoji: '🇩🇲' },
  { value: 'DO', label: 'Dominican Republic', emoji: '🇩🇴' },
  { value: 'DZ', label: 'Algeria', emoji: '🇩🇿' },
  { value: 'EC', label: 'Ecuador', emoji: '🇪🇨' },
  { value: 'EE', label: 'Estonia', emoji: '🇪🇪' },
  { value: 'EG', label: 'Egypt', emoji: '🇪🇬' },
  { value: 'ER', label: 'Eritrea', emoji: '🇪🇷' },
  { value: 'ES', label: 'Spain', emoji: '🇪🇸' },
  { value: 'ET', label: 'Ethiopia', emoji: '🇪🇹' },
  { value: 'FI', label: 'Finland', emoji: '🇫🇮' },
  { value: 'FJ', label: 'Fiji', emoji: '🇫🇯' },
  { value: 'FK', label: 'Falkland Islands', emoji: '🇫🇰' },
  { value: 'FM', label: 'Micronesia', emoji: '🇫🇲' },
  { value: 'FO', label: 'Faroe Islands', emoji: '🇫🇴' },
  { value: 'FR', label: 'France', emoji: '🇫🇷' },
  { value: 'GA', label: 'Gabon', emoji: '🇬🇦' },
  { value: 'GB', label: 'United Kingdom', emoji: '🇬🇧' },
  { value: 'GD', label: 'Grenada', emoji: '🇬🇩' },
  { value: 'GE', label: 'Georgia', emoji: '🇬🇪' },
  { value: 'GH', label: 'Ghana', emoji: '🇬🇭' },
  { value: 'GI', label: 'Gibraltar', emoji: '🇬🇮' },
  { value: 'GL', label: 'Greenland', emoji: '🇬🇱' },
  { value: 'GM', label: 'Gambia', emoji: '🇬🇲' },
  { value: 'GN', label: 'Guinea', emoji: '🇬🇳' },
  { value: 'GQ', label: 'Equatorial Guinea', emoji: '🇬🇶' },
  { value: 'GR', label: 'Greece', emoji: '🇬🇷' },
  { value: 'GT', label: 'Guatemala', emoji: '🇬🇹' },
  { value: 'GU', label: 'Guam', emoji: '🇬🇺' },
  { value: 'GW', label: 'Guinea-Bissau', emoji: '🇬🇼' },
  { value: 'GY', label: 'Guyana', emoji: '🇬🇾' },
  { value: 'HK', label: 'Hong Kong', emoji: '🇭🇰' },
  { value: 'HN', label: 'Honduras', emoji: '🇭🇳' },
  { value: 'HR', label: 'Croatia', emoji: '🇭🇷' },
  { value: 'HT', label: 'Haiti', emoji: '🇭🇹' },
  { value: 'HU', label: 'Hungary', emoji: '🇭🇺' },
  { value: 'ID', label: 'Indonesia', emoji: '🇮🇩' },
  { value: 'IE', label: 'Ireland', emoji: '🇮🇪' },
  { value: 'IL', label: 'Israel', emoji: '🇮🇱' },
  { value: 'IM', label: 'Isle of Man', emoji: '🇮🇲' },
  { value: 'IN', label: 'India', emoji: '🇮🇳' },
  { value: 'IQ', label: 'Iraq', emoji: '🇮🇶' },
  { value: 'IR', label: 'Iran', emoji: '🇮🇷' },
  { value: 'IS', label: 'Iceland', emoji: '🇮🇸' },
  { value: 'IT', label: 'Italy', emoji: '🇮🇹' },
  { value: 'JE', label: 'Jersey', emoji: '🇯🇪' },
  { value: 'JM', label: 'Jamaica', emoji: '🇯🇲' },
  { value: 'JO', label: 'Jordan', emoji: '🇯🇴' },
  { value: 'JP', label: 'Japan', emoji: '🇯🇵' },
  { value: 'KE', label: 'Kenya', emoji: '🇰🇪' },
  { value: 'KG', label: 'Kyrgyzstan', emoji: '🇰🇬' },
  { value: 'KH', label: 'Cambodia', emoji: '🇰🇭' },
  { value: 'KI', label: 'Kiribati', emoji: '🇰🇮' },
  { value: 'KM', label: 'Comoros', emoji: '🇰🇲' },
  { value: 'KN', label: 'Saint Kitts and Nevis', emoji: '🇰🇳' },
  { value: 'KP', label: 'North Korea', emoji: '🇰🇵' },
  { value: 'KR', label: 'South Korea', emoji: '🇰🇷' },
  { value: 'KW', label: 'Kuwait', emoji: '🇰🇼' },
  { value: 'KY', label: 'Cayman Islands', emoji: '🇰🇾' },
  { value: 'KZ', label: 'Kazakhstan', emoji: '🇰🇿' },
  { value: 'LA', label: 'Laos', emoji: '🇱🇦' },
  { value: 'LB', label: 'Lebanon', emoji: '🇱🇧' },
  { value: 'LC', label: 'Saint Lucia', emoji: '🇱🇨' },
  { value: 'LI', label: 'Liechtenstein', emoji: '🇱🇮' },
  { value: 'LK', label: 'Sri Lanka', emoji: '🇱🇰' },
  { value: 'LR', label: 'Liberia', emoji: '🇱🇷' },
  { value: 'LS', label: 'Lesotho', emoji: '🇱🇸' },
  { value: 'LT', label: 'Lithuania', emoji: '🇱🇹' },
  { value: 'LU', label: 'Luxembourg', emoji: '🇱🇺' },
  { value: 'LV', label: 'Latvia', emoji: '🇱🇻' },
  { value: 'LY', label: 'Libya', emoji: '🇱🇾' },
  { value: 'MA', label: 'Morocco', emoji: '🇲🇦' },
  { value: 'MC', label: 'Monaco', emoji: '🇲🇨' },
  { value: 'MD', label: 'Moldova', emoji: '🇲🇩' },
  { value: 'ME', label: 'Montenegro', emoji: '🇲🇪' },
  { value: 'MG', label: 'Madagascar', emoji: '🇲🇬' },
  { value: 'MH', label: 'Marshall Islands', emoji: '🇲🇭' },
  { value: 'MK', label: 'North Macedonia', emoji: '🇲🇰' },
  { value: 'ML', label: 'Mali', emoji: '🇲🇱' },
  { value: 'MM', label: 'Myanmar', emoji: '🇲🇲' },
  { value: 'MN', label: 'Mongolia', emoji: '🇲🇳' },
  { value: 'MO', label: 'Macao', emoji: '🇲🇴' },
  { value: 'MR', label: 'Mauritania', emoji: '🇲🇷' },
  { value: 'MS', label: 'Montserrat', emoji: '🇲🇸' },
  { value: 'MT', label: 'Malta', emoji: '🇲🇹' },
  { value: 'MU', label: 'Mauritius', emoji: '🇲🇺' },
  { value: 'MV', label: 'Maldives', emoji: '🇲🇻' },
  { value: 'MW', label: 'Malawi', emoji: '🇲🇼' },
  { value: 'MX', label: 'Mexico', emoji: '🇲🇽' },
  { value: 'MY', label: 'Malaysia', emoji: '🇲🇾' },
  { value: 'MZ', label: 'Mozambique', emoji: '🇲🇿' },
  { value: 'NA', label: 'Namibia', emoji: '🇳🇦' },
  { value: 'NC', label: 'New Caledonia', emoji: '🇳🇨' },
  { value: 'NE', label: 'Niger', emoji: '🇳🇪' },
  { value: 'NF', label: 'Norfolk Island', emoji: '🇳🇫' },
  { value: 'NG', label: 'Nigeria', emoji: '🇳🇬' },
  { value: 'NI', label: 'Nicaragua', emoji: '🇳🇮' },
  { value: 'NL', label: 'Netherlands', emoji: '🇳🇱' },
  { value: 'NO', label: 'Norway', emoji: '🇳🇴' },
  { value: 'NP', label: 'Nepal', emoji: '🇳🇵' },
  { value: 'NR', label: 'Nauru', emoji: '🇳🇷' },
  { value: 'NU', label: 'Niue', emoji: '🇳🇺' },
  { value: 'NZ', label: 'New Zealand', emoji: '🇳🇿' },
  { value: 'OM', label: 'Oman', emoji: '🇴🇲' },
  { value: 'PA', label: 'Panama', emoji: '🇵🇦' },
  { value: 'PE', label: 'Peru', emoji: '🇵🇪' },
  { value: 'PF', label: 'French Polynesia', emoji: '🇵🇫' },
  { value: 'PG', label: 'Papua New Guinea', emoji: '🇵🇬' },
  { value: 'PH', label: 'Philippines', emoji: '🇵🇭' },
  { value: 'PK', label: 'Pakistan', emoji: '🇵🇰' },
  { value: 'PL', label: 'Poland', emoji: '🇵🇱' },
  { value: 'PR', label: 'Puerto Rico', emoji: '🇵🇷' },
  { value: 'PS', label: 'Palestine', emoji: '🇵🇸' },
  { value: 'PT', label: 'Portugal', emoji: '🇵🇹' },
  { value: 'PW', label: 'Palau', emoji: '🇵🇼' },
  { value: 'PY', label: 'Paraguay', emoji: '🇵🇾' },
  { value: 'QA', label: 'Qatar', emoji: '🇶🇦' },
  { value: 'RO', label: 'Romania', emoji: '🇷🇴' },
  { value: 'RS', label: 'Serbia', emoji: '🇷🇸' },
  { value: 'RU', label: 'Russia', emoji: '🇷🇺' },
  { value: 'RW', label: 'Rwanda', emoji: '🇷🇼' },
  { value: 'SA', label: 'Saudi Arabia', emoji: '🇸🇦' },
  { value: 'SB', label: 'Solomon Islands', emoji: '🇸🇧' },
  { value: 'SC', label: 'Seychelles', emoji: '🇸🇨' },
  { value: 'SD', label: 'Sudan', emoji: '🇸🇩' },
  { value: 'SE', label: 'Sweden', emoji: '🇸🇪' },
  { value: 'SG', label: 'Singapore', emoji: '🇸🇬' },
  { value: 'SI', label: 'Slovenia', emoji: '🇸🇮' },
  { value: 'SK', label: 'Slovakia', emoji: '🇸🇰' },
  { value: 'SL', label: 'Sierra Leone', emoji: '🇸🇱' },
  { value: 'SM', label: 'San Marino', emoji: '🇸🇲' },
  { value: 'SN', label: 'Senegal', emoji: '🇸🇳' },
  { value: 'SO', label: 'Somalia', emoji: '🇸🇴' },
  { value: 'SR', label: 'Suriname', emoji: '🇸🇷' },
  { value: 'SS', label: 'South Sudan', emoji: '🇸🇸' },
  { value: 'ST', label: 'Sao Tome and Principe', emoji: '🇸🇹' },
  { value: 'SV', label: 'El Salvador', emoji: '🇸🇻' },
  { value: 'SY', label: 'Syria', emoji: '🇸🇾' },
  { value: 'SZ', label: 'Eswatini', emoji: '🇸🇿' },
  { value: 'TC', label: 'Turks and Caicos Islands', emoji: '🇹🇨' },
  { value: 'TD', label: 'Chad', emoji: '🇹🇩' },
  { value: 'TG', label: 'Togo', emoji: '🇹🇬' },
  { value: 'TH', label: 'Thailand', emoji: '🇹🇭' },
  { value: 'TJ', label: 'Tajikistan', emoji: '🇹🇯' },
  { value: 'TK', label: 'Tokelau', emoji: '🇹🇰' },
  { value: 'TL', label: 'Timor-Leste', emoji: '🇹🇱' },
  { value: 'TM', label: 'Turkmenistan', emoji: '🇹🇲' },
  { value: 'TN', label: 'Tunisia', emoji: '🇹🇳' },
  { value: 'TO', label: 'Tonga', emoji: '🇹🇴' },
  { value: 'TR', label: 'Türkiye', emoji: '🇹🇷' },
  { value: 'TT', label: 'Trinidad and Tobago', emoji: '🇹🇹' },
  { value: 'TV', label: 'Tuvalu', emoji: '🇹🇻' },
  { value: 'TW', label: 'Taiwan', emoji: '🇹🇼' },
  { value: 'TZ', label: 'Tanzania', emoji: '🇹🇿' },
  { value: 'UA', label: 'Ukraine', emoji: '🇺🇦' },
  { value: 'UG', label: 'Uganda', emoji: '🇺🇬' },
  { value: 'US', label: 'United States', emoji: '🇺🇸' },
  { value: 'UY', label: 'Uruguay', emoji: '🇺🇾' },
  { value: 'UZ', label: 'Uzbekistan', emoji: '🇺🇿' },
  { value: 'VA', label: 'Vatican City', emoji: '🇻🇦' },
  { value: 'VC', label: 'Saint Vincent and the Grenadines', emoji: '🇻🇨' },
  { value: 'VE', label: 'Venezuela', emoji: '🇻🇪' },
  { value: 'VG', label: 'British Virgin Islands', emoji: '🇻🇬' },
  { value: 'VI', label: 'U.S. Virgin Islands', emoji: '🇻🇮' },
  { value: 'VN', label: 'Vietnam', emoji: '🇻🇳' },
  { value: 'VU', label: 'Vanuatu', emoji: '🇻🇺' },
  { value: 'WF', label: 'Wallis and Futuna', emoji: '🇼🇫' },
  { value: 'WS', label: 'Samoa', emoji: '🇼🇸' },
  { value: 'YE', label: 'Yemen', emoji: '🇾🇪' },
  { value: 'YT', label: 'Mayotte', emoji: '🇾🇹' },
  { value: 'ZA', label: 'South Africa', emoji: '🇿🇦' },
  { value: 'ZM', label: 'Zambia', emoji: '🇿🇲' },
  { value: 'ZW', label: 'Zimbabwe', emoji: '🇿🇼' },
];

```

### Custom Object

Use the `itemToString` and `itemToValue` props to map custom objects to the required interface.

**Example: custom-object**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown, X } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

export component CustomObject() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems: [
        { country: 'United States', code: 'US', flag: '🇺🇸' },
        { country: 'Canada', code: 'CA', flag: '🇨🇦' },
        { country: 'Australia', code: 'AU', flag: '🇦🇺' },
      ],
      itemToString: (item) => item.country,
      itemToValue: (item) => item.code,
      filter: @filterApi.contains,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
    <Combobox.Label class={styles.Label}>{'Country'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. Canada" />
      <div class={styles.Indicators}>
        <Combobox.ClearTrigger class={styles.ClearTrigger}>
          <X />
        </Combobox.ClearTrigger>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item.code) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>
                {item.flag}
                {' '}
                {item.country}
              </Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

### Limit Results

Use the `limit` property on `useListCollection` to limit the number of rendered items in the DOM.

**Example: limit-results**

```ripple
import { Combobox, useListCollection } from 'ark-ripple/combobox';
import { useFilter } from 'ark-ripple/locale';
import { Portal } from 'ark-ripple/portal';
import { Check, ChevronsUpDown } from 'lucide-ripple';
import styles from 'styles/combobox.module.css';

const cities = [
  { label: 'New York', value: 'new-york' },
  { label: 'Los Angeles', value: 'los-angeles' },
  { label: 'Chicago', value: 'chicago' },
  { label: 'Houston', value: 'houston' },
  { label: 'Phoenix', value: 'phoenix' },
  { label: 'Philadelphia', value: 'philadelphia' },
  { label: 'San Antonio', value: 'san-antonio' },
  { label: 'San Diego', value: 'san-diego' },
  { label: 'Dallas', value: 'dallas' },
  { label: 'San Jose', value: 'san-jose' },
  { label: 'Austin', value: 'austin' },
  { label: 'Jacksonville', value: 'jacksonville' },
  { label: 'Fort Worth', value: 'fort-worth' },
  { label: 'Columbus', value: 'columbus' },
  { label: 'Charlotte', value: 'charlotte' },
  { label: 'San Francisco', value: 'san-francisco' },
  { label: 'Indianapolis', value: 'indianapolis' },
  { label: 'Seattle', value: 'seattle' },
  { label: 'Denver', value: 'denver' },
  { label: 'Boston', value: 'boston' },
];

export component LimitResults() {
  const filterApi = useFilter({ sensitivity: 'base' });

  let { collection, filter } = useListCollection(
    {
      initialItems: cities,
      limit: 5,
      filter: @filterApi.contains,
    },
  );

  const handleInputChange = (details: Combobox.InputValueChangeDetails) => {
    filter(details.inputValue);
  };

  <Combobox.Root class={styles.Root} {collection} onInputValueChange={handleInputChange}>
    <Combobox.Label class={styles.Label}>{'City'}</Combobox.Label>
    <Combobox.Control class={styles.Control}>
      <Combobox.Input class={styles.Input} placeholder="e.g. New York" />
      <div class={styles.Indicators}>
        <Combobox.Trigger class={styles.Trigger}>
          <ChevronsUpDown />
        </Combobox.Trigger>
      </div>
    </Combobox.Control>
    <Portal>
      <Combobox.Positioner>
        <Combobox.Content class={styles.Content}>
          for (const item of @collection.items; key item.value) {
            <Combobox.Item class={styles.Item} {item}>
              <Combobox.ItemText class={styles.ItemText}>{item.label}</Combobox.ItemText>
              <Combobox.ItemIndicator class={styles.ItemIndicator}>
                <Check />
              </Combobox.ItemIndicator>
            </Combobox.Item>
          }
        </Combobox.Content>
      </Combobox.Positioner>
    </Portal>
  </Combobox.Root>
}

```

## Guides

### Router Links

Customize the `navigate` prop on `Combobox.Root` to integrate with your router.:

```tsx
import { Combobox } from 'ark-ripple/combobox'
import { useNavigate } from '<some-router>'

function Demo() {
  const navigate = useNavigate()
  return (
    <Combobox.Root
      navigate={(e) => {
        navigate({ to: e.node.href })
      }}
    >
      {/* ... */}
    </Combobox.Root>
  )
}
```

### Custom Objects

By default, the combobox collection expects an array of objects with `label` and `value` properties. In some cases, you
may need to deal with custom objects.

Use the `itemToString` and `itemToValue` props to map the custom object to the required interface.

```tsx
const items = [
  { country: 'United States', code: 'US', flag: '🇺🇸' },
  { country: 'Canada', code: 'CA', flag: '🇨🇦' },
  { country: 'Australia', code: 'AU', flag: '🇦🇺' },
  // ...
]

const { collection } = useListCollection({
  initialItems: items,
  itemToString: (item) => item.country,
  itemToValue: (item) => item.code,
})
```

### Large Datasets

The recommended way of managing large lists is to use the `limit` property on the `useListCollection` hook. This will
limit the number of rendered items in the DOM to improve performance.

```tsx {3}
const { collection } = useListCollection({
  initialItems: items,
  limit: 10,
})
```

### Available Size

The following css variables are exposed to the `Combobox.Positioner` which you can use to style the `Combobox.Content`

```css
/* width of the combobox control */
--reference-width: <pixel-value>;
/* width of the available viewport */
--available-width: <pixel-value>;
/* height of the available viewport */
--available-height: <pixel-value>;
```

For example, if you want to make sure the maximum height doesn't exceed the available height, you can use the following:

```css
[data-scope='combobox'][data-part='content'] {
  max-height: calc(var(--available-height) - 100px);
}
```

## API Reference

### Props

**Component API Reference**

**Root Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `collection` | `ListCollection<T>` | Yes | The collection of items |
| `allowCustomValue` | `boolean` | No | Whether to allow typing custom values in the input |
| `alwaysSubmitOnEnter` | `boolean` | No | Whether to always submit on Enter key press, even if popup is open.
Useful for single-field autocomplete forms where Enter should submit the form. |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `autoFocus` | `boolean` | No | Whether to autofocus the input on mount |
| `closeOnSelect` | `boolean` | No | Whether to close the combobox when an item is selected. |
| `composite` | `boolean` | No | Whether the combobox is a composed with other composite widgets like tabs |
| `defaultHighlightedValue` | `string` | No | The initial highlighted value of the combobox when rendered.
Use when you don't need to control the highlighted value of the combobox. |
| `defaultInputValue` | `string` | No | The initial value of the combobox's input when rendered.
Use when you don't need to control the value of the combobox's input. |
| `defaultOpen` | `boolean` | No | The initial open state of the combobox when rendered.
Use when you don't need to control the open state of the combobox. |
| `defaultValue` | `string[]` | No | The initial value of the combobox's selected items when rendered.
Use when you don't need to control the value of the combobox's selected items. |
| `disabled` | `boolean` | No | Whether the combobox is disabled |
| `disableLayer` | `boolean` | No | Whether to disable registering this a dismissable layer |
| `form` | `string` | No | The associate form of the combobox. |
| `highlightedValue` | `string` | No | The controlled highlighted value of the combobox |
| `id` | `string` | No | The unique identifier of the machine. |
| `ids` | `Partial<{
  root: string
  label: string
  control: string
  input: string
  content: string
  trigger: string
  clearTrigger: string
  item: (id: string, index?: number | undefined) => string
  positioner: string
  itemGroup: (id: string | number) => string
  itemGroupLabel: (id: string | number) => string
}>` | No | The ids of the elements in the combobox. Useful for composition. |
| `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame |
| `inputBehavior` | `'none' | 'autohighlight' | 'autocomplete'` | No | Defines the auto-completion behavior of the combobox.

- `autohighlight`: The first focused item is highlighted as the user types
- `autocomplete`: Navigating the listbox with the arrow keys selects the item and the input is updated |
| `inputValue` | `string` | No | The controlled value of the combobox's input |
| `invalid` | `boolean` | No | Whether the combobox is invalid |
| `lazyMount` | `boolean` | No | Whether to enable lazy mounting |
| `loopFocus` | `boolean` | No | Whether to loop the keyboard navigation through the items |
| `multiple` | `boolean` | No | Whether to allow multiple selection.

**Good to know:** When `multiple` is `true`, the `selectionBehavior` is automatically set to `clear`.
It is recommended to render the selected items in a separate container. |
| `name` | `string` | No | The `name` attribute of the combobox's input. Useful for form submission |
| `navigate` | `(details: NavigateDetails) => void` | No | Function to navigate to the selected item |
| `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state |
| `onFocusOutside` | `(event: FocusOutsideEvent) => void` | No | Function called when the focus is moved outside the component |
| `onHighlightChange` | `(details: HighlightChangeDetails<T>) => void` | No | Function called when an item is highlighted using the pointer
or keyboard navigation. |
| `onInputValueChange` | `(details: InputValueChangeDetails) => void` | No | Function called when the input's value changes |
| `onInteractOutside` | `(event: InteractOutsideEvent) => void` | No | Function called when an interaction happens outside the component |
| `onOpenChange` | `(details: OpenChangeDetails) => void` | No | Function called when the popup is opened |
| `onPointerDownOutside` | `(event: PointerDownOutsideEvent) => void` | No | Function called when the pointer is pressed down outside the component |
| `onSelect` | `(details: SelectionDetails) => void` | No | Function called when an item is selected |
| `onValueChange` | `(details: ValueChangeDetails<T>) => void` | No | Function called when a new item is selected |
| `open` | `boolean` | No | The controlled open state of the combobox |
| `openOnChange` | `boolean | ((details: InputValueChangeDetails) => boolean)` | No | Whether to show the combobox when the input value changes |
| `openOnClick` | `boolean` | No | Whether to open the combobox popup on initial click on the input |
| `openOnKeyPress` | `boolean` | No | Whether to open the combobox on arrow key press |
| `placeholder` | `string` | No | The placeholder text of the combobox's input |
| `positioning` | `PositioningOptions` | No | The positioning options to dynamically position the menu |
| `present` | `boolean` | No | Whether the node is present (controlled by the user) |
| `readOnly` | `boolean` | No | Whether the combobox is readonly. This puts the combobox in a "non-editable" mode
but the user can still interact with it |
| `required` | `boolean` | No | Whether the combobox is required |
| `scrollToIndexFn` | `(details: ScrollToIndexDetails) => void` | No | Function to scroll to a specific index |
| `selectionBehavior` | `'clear' | 'replace' | 'preserve'` | No | The behavior of the combobox input when an item is selected

- `replace`: The selected item string is set as the input value
- `clear`: The input value is cleared
- `preserve`: The input value is preserved |
| `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. |
| `translations` | `IntlTranslations` | No | Specifies the localized strings that identifies the accessibility elements and their states |
| `unmountOnExit` | `boolean` | No | Whether to unmount on exit. |
| `value` | `string[]` | No | The controlled value of the combobox's selected items |


**Root Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | root |
| `[data-invalid]` | Present when invalid |
| `[data-readonly]` | Present when read-only |


**ClearTrigger Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ClearTrigger Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | clear-trigger |
| `[data-invalid]` | Present when invalid |


**Content Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Content Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | content |
| `[data-state]` | "open" | "closed" |
| `[data-nested]` | listbox |
| `[data-has-nested]` | listbox |
| `[data-placement]` | The placement of the content |
| `[data-empty]` | Present when the content is empty |


**Content CSS Variables:**

| Variable | Description |
|----------|-------------|
| `--layer-index` | The index of the dismissable in the layer stack |
| `--nested-layer-count` | The number of nested comboboxs |


**Control Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Control Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | control |
| `[data-state]` | "open" | "closed" |
| `[data-focus]` | Present when focused |
| `[data-disabled]` | Present when disabled |
| `[data-invalid]` | Present when invalid |


**Empty Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Input Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Input Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | input |
| `[data-invalid]` | Present when invalid |
| `[data-autofocus]` |  |
| `[data-state]` | "open" | "closed" |


**ItemGroupLabel Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemGroup Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemGroup Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | item-group |
| `[data-empty]` | Present when the content is empty |


**ItemIndicator Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemIndicator Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | item-indicator |
| `[data-state]` | "checked" | "unchecked" |


**Item Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `item` | `any` | No | The item to render |
| `persistFocus` | `boolean` | No | Whether hovering outside should clear the highlighted state |


**Item Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | item |
| `[data-highlighted]` | Present when highlighted |
| `[data-state]` | "checked" | "unchecked" |
| `[data-disabled]` | Present when disabled |
| `[data-value]` | The value of the item |


**ItemText Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ItemText Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | item-text |
| `[data-state]` | "checked" | "unchecked" |
| `[data-disabled]` | Present when disabled |
| `[data-highlighted]` | Present when highlighted |


**Label Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Label Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | label |
| `[data-readonly]` | Present when read-only |
| `[data-disabled]` | Present when disabled |
| `[data-invalid]` | Present when invalid |
| `[data-required]` | Present when required |
| `[data-focus]` | Present when focused |


**List Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**List Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | list |
| `[data-empty]` | Present when the content is empty |


**Positioner Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Positioner CSS Variables:**

| Variable | Description |
|----------|-------------|
| `--reference-width` | The width of the reference element |
| `--reference-height` | The height of the root |
| `--available-width` | The available width in viewport |
| `--available-height` | The available height in viewport |
| `--x` | The x position for transform |
| `--y` | The y position for transform |
| `--z-index` | The z-index value |
| `--transform-origin` | The transform origin for animations |


**RootProvider Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `value` | `UseComboboxReturn<T>` | Yes |  |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame |
| `lazyMount` | `boolean` | No | Whether to enable lazy mounting |
| `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state |
| `present` | `boolean` | No | Whether the node is present (controlled by the user) |
| `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. |
| `unmountOnExit` | `boolean` | No | Whether to unmount on exit. |


**Trigger Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `focusable` | `boolean` | No | Whether the trigger is focusable |


**Trigger Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | combobox |
| `[data-part]` | trigger |
| `[data-state]` | "open" | "closed" |
| `[data-invalid]` | Present when invalid |
| `[data-focusable]` |  |
| `[data-readonly]` | Present when read-only |
| `[data-disabled]` | Present when disabled |


### Context

**API:**

| Property | Type | Description |
|----------|------|-------------|
| `focused` | `boolean` | Whether the combobox is focused |
| `open` | `boolean` | Whether the combobox is open |
| `inputValue` | `string` | The value of the combobox input |
| `highlightedValue` | `string` | The value of the highlighted item |
| `highlightedItem` | `V` | The highlighted item |
| `setHighlightValue` | `(value: string) => void` | The value of the combobox input |
| `clearHighlightValue` | `VoidFunction` | Function to clear the highlighted value |
| `syncSelectedItems` | `VoidFunction` | Function to sync the selected items with the value.
Useful when `value` is updated from async sources. |
| `selectedItems` | `V[]` | The selected items |
| `hasSelectedItems` | `boolean` | Whether there's a selected item |
| `value` | `string[]` | The selected item keys |
| `valueAsString` | `string` | The string representation of the selected items |
| `selectValue` | `(value: string) => void` | Function to select a value |
| `setValue` | `(value: string[]) => void` | Function to set the value of the combobox |
| `clearValue` | `(value?: string) => void` | Function to clear the value of the combobox |
| `focus` | `VoidFunction` | Function to focus on the combobox input |
| `setInputValue` | `(value: string, reason?: InputValueChangeReason) => void` | Function to set the input value of the combobox |
| `getItemState` | `(props: ItemProps) => ItemState` | Returns the state of a combobox item |
| `setOpen` | `(open: boolean, reason?: OpenChangeReason) => void` | Function to open or close the combobox |
| `collection` | `ListCollection<V>` | Function to toggle the combobox |
| `reposition` | `(options?: Partial<PositioningOptions>) => void` | Function to set the positioning options |
| `multiple` | `boolean` | Whether the combobox allows multiple selections |
| `disabled` | `boolean` | Whether the combobox is disabled |


## Accessibility

Complies with the [Combobox WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).

### Keyboard Support

<KeyBindingsTable id="combobox" />