# Combobox

An autocomplete input and popup picker supporting single selection, multi-tag picking with chips, and rich item rows.

```slint
import { Button } from "@glint/components/button.slint";
import { Combobox, ComboboxItem } from "@glint/components/combobox.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 560px;
    height: 360px;
    background: Tokens.color-background;

    in-out property <int> selected-idx: 0;

    VerticalLayout {
        alignment: center;
        spacing: 16px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            combo := Combobox {
                selected-index <=> root.selected-idx;
                items: [
                    { value: "next", label: "Next.js", icon: IconSet.Globe, description: "React Fullstack Framework" },
                    { value: "astro", label: "Astro", icon: IconSet.Rocket, description: "Content-driven Web Framework" },
                    { value: "slint", label: "Slint", icon: IconSet.Layout, description: "Native GUI Toolkit" },
                    { value: "vite", label: "Vite", icon: IconSet.Zap, description: "Frontend Tooling" },
                ];

                Button {
                    text: root.selected-idx >= 0 ? "Framework: " + combo.items[root.selected-idx].label : "Select framework…";
                    haspopup: true;
                    clicked => { combo.show(); }
                }
            }
        }

        Text {
            text: "Chosen index: " + root.selected-idx;
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }
    }
}
```

## Usage

```slint
import { Button } from "@glint/components/button.slint";
import { Combobox, ComboboxItem } from "@glint/components/combobox.slint";

export component AppWindow inherits Window {
    in-out property <int> active-index: -1;

    VerticalLayout {
        alignment: center;

        combo := Combobox {
            selected-index <=> root.active-index;
            items: [
                { value: "rust", label: "Rust" },
                { value: "ts", label: "TypeScript" },
                { value: "slint", label: "Slint" },
            ];

            Button {
                text: "Select Language";
                haspopup: true;
                clicked => { combo.show(); }
            }
        }
    }
}
```

`Combobox` wraps a custom trigger in its `@children` slot. Calling `show()` opens a popup containing a search input over a scrollable suggestions list. Typing in the search input updates the two-way `query` property and dynamically drives typeahead navigation.

When `multiple: true` is set, `Combobox` operates as a multi-selection tag picker where items can be toggled without closing the dropdown.

## Examples

### Multi-selection tag picker with chips

Set `multiple: true` and bind `selected` (an array of booleans matching `items` length). Use `ComboboxChips` within or beside the trigger to display removable badges for selected items.

```slint
import { Button } from "@glint/components/button.slint";
import { Combobox, ComboboxChips } from "@glint/components/combobox.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 360px;
    background: Tokens.color-background;

    in-out property <[bool]> tag-selected: [true, false];
    in-out property <[string]> chosen-labels: ["Bug"];

    function sync-chosen-labels() {
        if (root.tag-selected[0]) {
            root.chosen-labels = root.tag-selected[1]
                ? ["Bug", "Documentation"] : ["Bug"];
        } else {
            root.chosen-labels = root.tag-selected[1]
                ? ["Documentation"] : [];
        }
    }

    VerticalLayout {
        alignment: center;
        spacing: 16px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;
            spacing: 8px;

            combo := Combobox {
                multiple: true;
                selected <=> root.tag-selected;
                items: [
                    { value: "bug", label: "Bug" },
                    { value: "docs", label: "Documentation" },
                ];
                changed(index) => {
                    root.sync-chosen-labels();
                }

                Button {
                    text: "Select Tags…";
                    haspopup: true;
                    clicked => { combo.show(); }
                }
            }
        }

        HorizontalLayout {
            alignment: center;

            ComboboxChips {
                labels: root.chosen-labels;
                removed(idx) => {
                    if (root.chosen-labels[idx] == "Bug") {
                        root.tag-selected[0] = false;
                    } else {
                        root.tag-selected[1] = false;
                    }
                    root.sync-chosen-labels();
                }
            }
        }
    }
}
```

### Rich item rows with icons, descriptions, and badges

`ComboboxItem` supports rich metadata including leading icons (`icon`) or avatars (`avatar`), secondary subtext (`description`), and trailing status badges (`badge`).

```slint
import { Button } from "@glint/components/button.slint";
import { Combobox } from "@glint/components/combobox.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 560px;
    height: 360px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            combo := Combobox {
                content-width: 360px;
                items: [
                    { value: "v1", label: "Version 1.0", icon: IconSet.Tag, description: "Legacy release", badge: "Deprecated" },
                    { value: "v2", label: "Version 2.0", icon: IconSet.Tag, description: "Stable LTS release", badge: "Stable" },
                    { value: "v3", label: "Version 3.0", icon: IconSet.Sparkles, description: "Next generation build", badge: "Beta" },
                ];

                Button {
                    text: "Select Release Version";
                    haspopup: true;
                    clicked => { combo.show(); }
                }
            }
        }
    }
}
```

### Grouped suggestions

Following the [grouped rows](/docs/components/select#grouped-rows) model, use `separator-before` and `heading-before` on items to organize large option sets into titled categories without shifting item indices.

```slint
import { Button } from "@glint/components/button.slint";
import { Combobox } from "@glint/components/combobox.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 560px;
    height: 360px;
    background: Tokens.color-background;

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            combo := Combobox {
                items: [
                    { value: "chrome", label: "Google Chrome", icon: IconSet.Globe, heading-before: "Web Browsers" },
                    { value: "firefox", label: "Mozilla Firefox", icon: IconSet.Globe },
                    { value: "safari", label: "Apple Safari", icon: IconSet.Globe },
                    { value: "vscode", label: "Visual Studio Code", icon: IconSet.Code, separator-before: true, heading-before: "Development Tools" },
                    { value: "terminal", label: "Terminal", icon: IconSet.Terminal },
                ];

                Button {
                    text: "Select Application";
                    haspopup: true;
                    clicked => { combo.show(); }
                }
            }
        }
    }
}
```

## API Reference

### Properties

| Property            | Type                | Default                    | Description                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------- | ------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items`             | `in [ComboboxItem]` | no default                 | Options shown in the popup; `value` is yours to use, Glint only reads `label`.                                                                                                                                                                                                                                                                                                                                                  |
| `selected-index`    | `in-out int`        | `-1`                       | Two-way; index of the chosen item in `items`, or -1 for no selection. Single-selection mode only — see `multiple`.                                                                                                                                                                                                                                                                                                              |
| `multiple`          | `in bool`           | `false`                    | Several rows chosen at once rather than one: the tag picker. Rows announce themselves checkable, choosing one leaves the popup open so the next can be chosen without reopening, and the chosen values are drawn by a `ComboboxChips` the consumer puts inside its own trigger.                                                                                                                                                 |
| `selected`          | `in-out [bool]`     | `[]`                       | Two-way; which rows are chosen in `multiple` mode, one flag per row of `items` in the same order. Size it to `items`: Slint cannot grow an array, so a flag past the end of this list is a choice with nowhere to land. A consumer who filters `items` by the query derives this beside it — which is also why the chips read their own list of labels rather than this one, and go on reading it while a query hides the rows. |
| `query`             | `in-out string`     | no default                 | Two-way; current text in the search field. Reset to "" to clear the filter.                                                                                                                                                                                                                                                                                                                                                     |
| `highlighted-index` | `in-out int`        | `0`                        | Row highlighted by Enter; consumers rarely set this directly.                                                                                                                                                                                                                                                                                                                                                                   |
| `placeholder`       | `in string`         | `@tr("Search…")`           | Hint shown in the search field when empty.                                                                                                                                                                                                                                                                                                                                                                                      |
| `empty-text`        | `in string`         | `@tr("No results found.")` | Shown in place of the list when `items` is empty — the state a filtered popup lands in most often. Set it to "" to show nothing at all.                                                                                                                                                                                                                                                                                         |
| `clear-label`       | `in string`         | `@tr("Clear search")`      | Name of the control that empties the query, for assistive technology.                                                                                                                                                                                                                                                                                                                                                           |
| `content-width`     | `in length`         | `320px`                    | Pixel width of the popup panel.                                                                                                                                                                                                                                                                                                                                                                                                 |
| `disabled`          | `in bool`           | `false`                    | When true, the popup stays shut and the control announces itself unavailable. The trigger is `@children` and its own look belongs to it (ADR-0006) — bind the same flag there to dim it.                                                                                                                                                                                                                                        |
| `is-open`           | `out bool`          | no default                 | True while the popup is on screen. The trigger is `@children`, so rotating its chevron is yours to do — bind the rotation to this.                                                                                                                                                                                                                                                                                              |

### Callbacks

| Callback       | Description                                       |
| -------------- | ------------------------------------------------- |
| `changed(int)` | Fired with the chosen item's index when selected. |

### Functions

| Function  | Description                                                                                                                                                                                                                                                                                                                                                                                                                      |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `show()`  | Opens the popup with the highlight back at the top. Mirrors `PopupWindow.show()` — wire it to a consumer-owned trigger so the popup opens from the keyboard as well as from a click. The `disabled` guard lives here rather than on each caller, because a consumer cannot suppress a popup from outside once it is up: every way in — the pointer, this function, both accessible actions — has to be refused at the same door. |
| `close()` | Closes the popup. Mirrors `PopupWindow.close()`.                                                                                                                                                                                                                                                                                                                                                                                 |

### ComboboxChips

A horizontal strip rendering selected values as individual tags with accessible remove buttons.

### Properties

| Property       | Type          | Default         | Description                                                                                                                                                                                                                                                                         |
| -------------- | ------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `labels`       | `in [string]` | no default      | What is chosen, in the order the chips read.                                                                                                                                                                                                                                        |
| `remove-label` | `in string`   | `@tr("Remove")` | How the control that drops a chip is named. It is joined with the chip's own label, so each control says which value it drops rather than leaving a row of identical "Remove" buttons — Slint has no way to place the value inside an overridable sentence, so the join is a space. |

### Callbacks

| Callback       | Description                                                      |
| -------------- | ---------------------------------------------------------------- |
| `removed(int)` | Fired with the index into `labels` of the chip that was dropped. |

## Accessibility

- **Combobox semantics.** The trigger node exposes the `combobox` role, announcing expanded/collapsed states and current value.
- **Multi-select state.** In `multiple` mode, list items announce themselves as checkable with their checked boolean state.
- **Rich content announcement.** Item descriptions and badges ride as `accessible-description` on the list item rather than creating noisy secondary nodes.
- **Chip controls.** Each chip in `ComboboxChips` includes a remove button with an accessible label explicitly naming the item to be removed (e.g. `Remove Bug`).
- **Keyboard navigation.** `↑`/`↓` navigate items, `Home`/`End` jump to list ends, and typing in the search input runs query-driven prefix navigation.
