View as Markdown

Combobox

Autocomplete input and command palette with a list of suggestions.

Loading...
{% set packages = [
    {value: 'turbo', label: 'UX Turbo'},
    {value: 'twig-component', label: 'UX Twig Component'},
    {value: 'live-component', label: 'UX Live Component'},
    {value: 'autocomplete', label: 'UX Autocomplete'},
    {value: 'icons', label: 'UX Icons'},
] %}
<twig:Combobox
    id="package"
    placeholder="Select package..."
    searchPlaceholder="Search packages..."
    :choices="packages"
/>

Installation

php bin/console ux:install combobox --kit shadcn

Install the following Composer dependencies:

composer require symfony/ux-icons tales-from-a-dev/twig-tailwind-extra:^1.0.0

Copy the following file(s) into your app:

import { Controller } from &#039;@hotwired/stimulus&#039;;

export default class extends Controller {
    static targets = [
        &#039;trigger&#039;,
        &#039;label&#039;,
        &#039;popover&#039;,
        &#039;search&#039;,
        &#039;option&#039;,
        &#039;empty&#039;,
        &#039;hiddenInput&#039;,
        &#039;group&#039;,
        &#039;clearButton&#039;,
    ];

    static values = {
        value: { type: String, default: &#039;&#039; },
        placeholder: { type: String, default: &#039;Select option...&#039; },
    };

    #activeIndex = -1;
    #isOpen = false;
    #outsideClickHandler = null;

    connect() {
        this.#outsideClickHandler = this.#onOutsideClick.bind(this);
    }

    disconnect() {
        this.#close();
    }

    toggle() {
        if (this.#isOpen) {
            this.#close();
        } else {
            this.#open();
        }
    }

    clear(event) {
        event.stopPropagation();
        this.valueValue = &#039;&#039;;
    }

    onSearch(event) {
        const query = event.target.value.toLowerCase();
        let firstVisibleIndex = -1;
        let visibleCount = 0;

        const targets = this.optionTargets;
        for (let i = 0; i &lt; targets.length; i++) {
            const matches = targets[i].dataset.label.toLowerCase().includes(query);
            targets[i].hidden = !matches;
            if (matches) {
                if (firstVisibleIndex === -1) firstVisibleIndex = i;
                visibleCount++;
            }
        }

        for (const group of this.groupTargets) {
            group.hidden = !targets.filter((o) =&gt; group.contains(o)).some((o) =&gt; !o.hidden);
        }

        this.emptyTarget.hidden = visibleCount &gt; 0;
        this.#setActive(firstVisibleIndex);
    }

    onSelect(event) {
        this.#selectOption(event.currentTarget);
    }

    onOptionHover(event) {
        const index = this.optionTargets.indexOf(event.currentTarget);
        if (index !== -1 &amp;&amp; !event.currentTarget.hidden) {
            this.#setActive(index);
        }
    }

    onTriggerKeydown(event) {
        switch (event.key) {
            case &#039;ArrowDown&#039;:
                event.preventDefault();
                this.#open();
                this.#setActive(this.#firstVisibleIndex());
                break;
            case &#039;ArrowUp&#039;:
                event.preventDefault();
                this.#open();
                this.#setActive(this.#lastVisibleIndex());
                break;
            case &#039;Enter&#039;:
            case &#039; &#039;:
                event.preventDefault();
                this.#open();
                break;
        }
    }

    onSearchKeydown(event) {
        switch (event.key) {
            case &#039;ArrowDown&#039;: {
                event.preventDefault();
                const next = this.#nextVisibleIndex(this.#activeIndex);
                if (next !== -1) this.#setActive(next);
                break;
            }
            case &#039;ArrowUp&#039;: {
                event.preventDefault();
                const prev = this.#prevVisibleIndex(this.#activeIndex);
                if (prev !== -1) this.#setActive(prev);
                break;
            }
            case &#039;Home&#039;:
                event.preventDefault();
                this.#setActive(this.#firstVisibleIndex());
                break;
            case &#039;End&#039;:
                event.preventDefault();
                this.#setActive(this.#lastVisibleIndex());
                break;
            case &#039;Enter&#039;:
                event.preventDefault();
                if (this.#activeIndex !== -1) {
                    this.#selectOption(this.optionTargets[this.#activeIndex]);
                }
                break;
            case &#039;Escape&#039;:
                event.preventDefault();
                this.#close();
                this.triggerTarget.focus();
                break;
            case &#039;Tab&#039;:
                this.#close();
                break;
        }
    }

    valueValueChanged() {
        this.#syncLabel();
        this.#syncCheckIcons();
        if (this.hasHiddenInputTarget) {
            this.hiddenInputTarget.value = this.valueValue;
        }
    }

    #open() {
        if (this.#isOpen) return;
        this.#isOpen = true;

        this.searchTarget.value = &#039;&#039;;
        for (const option of this.optionTargets) {
            option.hidden = false;
        }
        for (const group of this.groupTargets) {
            group.hidden = false;
        }
        this.emptyTarget.hidden = true;
        this.#setActive(-1);

        const popover = this.popoverTarget;
        popover.hidden = false;
        popover.dataset.state = &#039;open&#039;;
        this.#positionPopover();
        this.triggerTarget.setAttribute(&#039;aria-expanded&#039;, &#039;true&#039;);

        document.addEventListener(&#039;pointerdown&#039;, this.#outsideClickHandler);

        requestAnimationFrame(() =&gt; {
            this.searchTarget.focus();
        });
    }

    #close() {
        if (!this.#isOpen) return;
        this.#isOpen = false;

        const popover = this.popoverTarget;
        popover.hidden = true;
        popover.dataset.state = &#039;closed&#039;;
        popover.style.cssText = &#039;&#039;;
        this.triggerTarget.setAttribute(&#039;aria-expanded&#039;, &#039;false&#039;);

        document.removeEventListener(&#039;pointerdown&#039;, this.#outsideClickHandler);
    }

    #selectOption(option) {
        const { value, label } = option.dataset;
        this.valueValue = value;
        this.dispatch(&#039;change&#039;, { detail: { value, label }, bubbles: true });
        this.#close();
        this.triggerTarget.focus();
    }

    #syncLabel() {
        if (!this.hasLabelTarget) return;
        const selected = this.hasOptionTarget
            ? this.optionTargets.find((o) =&gt; o.dataset.value === this.valueValue)
            : null;
        const label = selected ? selected.dataset.label : &#039;&#039;;
        this.labelTarget.textContent = label || this.placeholderValue;
        this.labelTarget.classList.toggle(&#039;text-muted-foreground&#039;, !label);
        if (this.hasClearButtonTarget) {
            this.clearButtonTarget.hidden = !label;
        }
    }

    #syncCheckIcons() {
        if (!this.hasOptionTarget) return;
        for (const option of this.optionTargets) {
            const selected = option.dataset.value === this.valueValue;
            option.setAttribute(&#039;aria-selected&#039;, String(selected));
            const icon = option.querySelector(&#039;svg&#039;);
            if (icon) {
                icon.classList.toggle(&#039;opacity-0&#039;, !selected);
                icon.classList.toggle(&#039;opacity-100&#039;, selected);
            }
        }
    }

    #setActive(index) {
        if (this.#activeIndex !== -1 &amp;&amp; this.optionTargets[this.#activeIndex]) {
            delete this.optionTargets[this.#activeIndex].dataset.active;
        }

        this.#activeIndex = index;

        if (index === -1) {
            if (this.hasSearchTarget) {
                this.searchTarget.removeAttribute(&#039;aria-activedescendant&#039;);
            }
            return;
        }

        const option = this.optionTargets[index];
        if (!option) return;

        option.dataset.active = &#039;&#039;;
        this.searchTarget.setAttribute(&#039;aria-activedescendant&#039;, option.id);
        option.scrollIntoView({ block: &#039;nearest&#039; });
    }

    #firstVisibleIndex() {
        return this.optionTargets.findIndex((o) =&gt; !o.hidden);
    }

    #lastVisibleIndex() {
        const targets = this.optionTargets;
        for (let i = targets.length - 1; i &gt;= 0; i--) {
            if (!targets[i].hidden) return i;
        }
        return -1;
    }

    #nextVisibleIndex(from) {
        const targets = this.optionTargets;
        for (let i = from + 1; i &lt; targets.length; i++) {
            if (!targets[i].hidden) return i;
        }
        return from;
    }

    #prevVisibleIndex(from) {
        const targets = this.optionTargets;
        for (let i = from - 1; i &gt;= 0; i--) {
            if (!targets[i].hidden) return i;
        }
        return from;
    }

    #positionPopover() {
        const triggerRect = this.triggerTarget.getBoundingClientRect();
        const popover = this.popoverTarget;
        const popoverHeight = popover.offsetHeight;

        popover.style.position = &#039;fixed&#039;;
        popover.style.width = `${triggerRect.width}px`;
        popover.style.left = `${triggerRect.left}px`;
        popover.style.zIndex = &#039;50&#039;;

        const spaceBelow = window.innerHeight - triggerRect.bottom;
        if (spaceBelow &lt; popoverHeight &amp;&amp; triggerRect.top &gt; spaceBelow) {
            popover.style.top = `${Math.max(0, triggerRect.top - popoverHeight)}px`;
        } else {
            popover.style.top = `${triggerRect.bottom}px`;
        }
    }

    #onOutsideClick(event) {
        if (!this.element.contains(event.target) &amp;&amp; !this.popoverTarget.contains(event.target)) {
            this.#close();
        }
    }
}
{# @prop id string Unique identifier for ARIA references and internal element IDs. #}
{# @prop name string|null If set, renders a hidden input for form submission. #}
{# @prop value string The initially selected value. Must match a choices entry. #}
{# @prop choices array List of choices: `[{value: &#039;...&#039;, label: &#039;...&#039;}]` or grouped `[{label: &#039;...&#039;, choices: [{value: &#039;...&#039;, label: &#039;...&#039;}]}]`. #}
{# @prop placeholder string Text shown on the trigger when nothing is selected. #}
{# @prop searchPlaceholder string Placeholder inside the search input. #}
{# @prop emptyMessage string Shown when the filter matches nothing. #}
{# @prop disabled boolean Whether the widget is disabled. #}
{# @prop required boolean Adds required to the hidden input (only applies when name is set). #}
{# @prop clearable boolean Shows a clear button when a value is selected. #}
{%- props id, name = null, value = &#039;&#039;, choices = [], placeholder = &#039;Select option...&#039;, searchPlaceholder = &#039;Search...&#039;, emptyMessage = &#039;No results found.&#039;, disabled = false, required = false, clearable = false -%}

{%- set _is_grouped = choices is not empty and choices|first.choices is defined -%}
{%- set _selected_label = &#039;&#039; -%}
{%- if _is_grouped -%}
    {%- for group in choices -%}
        {%- for choice in group.choices -%}
            {%- if choice.value == value -%}{%- set _selected_label = choice.label -%}{%- endif -%}
        {%- endfor -%}
    {%- endfor -%}
{%- else -%}
    {%- for choice in choices -%}
        {%- if choice.value == value -%}{%- set _selected_label = choice.label -%}{%- endif -%}
    {%- endfor -%}
{%- endif -%}

&lt;div
    data-slot=&quot;combobox&quot;
    data-controller=&quot;combobox&quot;
    data-combobox-value-value=&quot;{{ value }}&quot;
    data-combobox-placeholder-value=&quot;{{ placeholder }}&quot;
    {{ attributes }}
&gt;
    &lt;button
        type=&quot;button&quot;
        role=&quot;combobox&quot;
        aria-expanded=&quot;false&quot;
        aria-controls=&quot;{{ id }}_listbox&quot;
        aria-haspopup=&quot;listbox&quot;
        data-combobox-target=&quot;trigger&quot;
        data-slot=&quot;combobox-trigger&quot;
        data-action=&quot;click-&gt;combobox#toggle keydown-&gt;combobox#onTriggerKeydown&quot;
        class=&quot;{{ (&#039;flex h-9 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50&#039;)|tailwind_merge }}&quot;
        {% if disabled %}disabled{% endif %}
    &gt;
        &lt;span
            data-combobox-target=&quot;label&quot;
            class=&quot;{{ _selected_label ? &#039;&#039; : &#039;text-muted-foreground&#039; }}&quot;
        &gt;{{ _selected_label ?: placeholder }}&lt;/span&gt;
        &lt;span class=&quot;flex shrink-0 items-center gap-1&quot;&gt;
            {% if clearable %}
                &lt;span
                    aria-label=&quot;Clear selection&quot;
                    data-combobox-target=&quot;clearButton&quot;
                    data-action=&quot;click-&gt;combobox#clear&quot;
                    data-slot=&quot;combobox-clear&quot;
                    {% if not _selected_label %}hidden{% endif %}
                    class=&quot;rounded-sm opacity-50 hover:opacity-100&quot;
                &gt;&lt;twig:ux:icon name=&quot;lucide:x&quot; class=&quot;size-4 shrink-0&quot; /&gt;&lt;/span&gt;
            {% endif %}
            &lt;twig:ux:icon name=&quot;lucide:chevrons-up-down&quot; class=&quot;size-4 shrink-0 opacity-50&quot; /&gt;
        &lt;/span&gt;
    &lt;/button&gt;

    {% if name is not null %}
        &lt;input
            type=&quot;hidden&quot;
            name=&quot;{{ name }}&quot;
            value=&quot;{{ value }}&quot;
            data-combobox-target=&quot;hiddenInput&quot;
            {% if required %}required{% endif %}
        /&gt;
    {% endif %}

    &lt;div
        id=&quot;{{ id }}_popover&quot;
        data-combobox-target=&quot;popover&quot;
        data-slot=&quot;combobox-content&quot;
        data-state=&quot;closed&quot;
        hidden
        class=&quot;z-50 min-w-32 overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md&quot;
    &gt;
        &lt;div class=&quot;flex items-center border-b border-border px-3&quot; data-slot=&quot;combobox-input-wrapper&quot;&gt;
            &lt;twig:ux:icon name=&quot;lucide:search&quot; class=&quot;mr-2 size-4 shrink-0 opacity-50&quot; /&gt;
            &lt;input
                type=&quot;text&quot;
                role=&quot;searchbox&quot;
                aria-controls=&quot;{{ id }}_listbox&quot;
                aria-autocomplete=&quot;list&quot;
                placeholder=&quot;{{ searchPlaceholder }}&quot;
                data-combobox-target=&quot;search&quot;
                data-action=&quot;input-&gt;combobox#onSearch keydown-&gt;combobox#onSearchKeydown&quot;
                class=&quot;{{ (&#039;flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50&#039;)|tailwind_merge }}&quot;
            /&gt;
        &lt;/div&gt;
        &lt;div
            id=&quot;{{ id }}_listbox&quot;
            role=&quot;listbox&quot;
            data-slot=&quot;combobox-list&quot;
            class=&quot;max-h-[300px] overflow-x-hidden overflow-y-auto p-1&quot;
        &gt;
            {% if _is_grouped %}
                {% for group in choices %}
                    &lt;div
                        role=&quot;group&quot;
                        aria-labelledby=&quot;{{ id }}_group_{{ loop.index0 }}&quot;
                        data-combobox-target=&quot;group&quot;
                        data-slot=&quot;combobox-group&quot;
                    &gt;
                        &lt;div
                            id=&quot;{{ id }}_group_{{ loop.index0 }}&quot;
                            data-slot=&quot;combobox-group-label&quot;
                            class=&quot;px-2 py-1.5 text-xs font-medium text-muted-foreground&quot;
                        &gt;{{ group.label }}&lt;/div&gt;
                        {% for choice in group.choices %}
                            &lt;div
                                role=&quot;option&quot;
                                id=&quot;{{ id }}_option_{{ loop.parent.loop.index0 }}_{{ loop.index0 }}&quot;
                                data-combobox-target=&quot;option&quot;
                                data-value=&quot;{{ choice.value }}&quot;
                                data-label=&quot;{{ choice.label }}&quot;
                                data-slot=&quot;combobox-item&quot;
                                aria-selected=&quot;{{ choice.value == value ? &#039;true&#039; : &#039;false&#039; }}&quot;
                                data-action=&quot;click-&gt;combobox#onSelect mouseenter-&gt;combobox#onOptionHover&quot;
                                class=&quot;relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[active]:bg-accent data-[active]:text-accent-foreground&quot;
                            &gt;
                                &lt;twig:ux:icon name=&quot;lucide:check&quot; class=&quot;size-4 {{ choice.value == value ? &#039;opacity-100&#039; : &#039;opacity-0&#039; }}&quot; /&gt;
                                {{ choice.label }}
                            &lt;/div&gt;
                        {% endfor %}
                    &lt;/div&gt;
                {% endfor %}
            {% else %}
                {% for choice in choices %}
                    &lt;div
                        role=&quot;option&quot;
                        id=&quot;{{ id }}_option_{{ loop.index0 }}&quot;
                        data-combobox-target=&quot;option&quot;
                        data-value=&quot;{{ choice.value }}&quot;
                        data-label=&quot;{{ choice.label }}&quot;
                        data-slot=&quot;combobox-item&quot;
                        aria-selected=&quot;{{ choice.value == value ? &#039;true&#039; : &#039;false&#039; }}&quot;
                        data-action=&quot;click-&gt;combobox#onSelect mouseenter-&gt;combobox#onOptionHover&quot;
                        class=&quot;relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[active]:bg-accent data-[active]:text-accent-foreground&quot;
                    &gt;
                        &lt;twig:ux:icon name=&quot;lucide:check&quot; class=&quot;size-4 {{ choice.value == value ? &#039;opacity-100&#039; : &#039;opacity-0&#039; }}&quot; /&gt;
                        {{ choice.label }}
                    &lt;/div&gt;
                {% endfor %}
            {% endif %}
            &lt;div
                data-combobox-target=&quot;empty&quot;
                data-slot=&quot;combobox-empty&quot;
                hidden
                class=&quot;py-6 text-center text-sm text-muted-foreground&quot;
            &gt;{{ emptyMessage }}&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

Usage

{% set packages = [
    {value: 'turbo', label: 'UX Turbo'},
    {value: 'twig-component', label: 'UX Twig Component'},
    {value: 'live-component', label: 'UX Live Component'},
    {value: 'autocomplete', label: 'UX Autocomplete'},
    {value: 'icons', label: 'UX Icons'},
] %}
<twig:Combobox
    id="package-usage"
    placeholder="Select package..."
    searchPlaceholder="Search packages..."
    :choices="packages"
/>

Examples

With Default Value

Loading...
{% set packages = [
    {value: 'turbo', label: 'UX Turbo'},
    {value: 'twig-component', label: 'UX Twig Component'},
    {value: 'live-component', label: 'UX Live Component'},
    {value: 'autocomplete', label: 'UX Autocomplete'},
    {value: 'icons', label: 'UX Icons'},
] %}
<twig:Combobox
    id="package-default"
    value="live-component"
    placeholder="Select package..."
    searchPlaceholder="Search packages..."
    :choices="packages"
/>

With Form

Loading...
{% set packages = [
    {value: 'turbo', label: 'UX Turbo'},
    {value: 'twig-component', label: 'UX Twig Component'},
    {value: 'live-component', label: 'UX Live Component'},
    {value: 'autocomplete', label: 'UX Autocomplete'},
    {value: 'icons', label: 'UX Icons'},
] %}
<form method="post" action="#" class="flex max-w-xs flex-col gap-4">
    <twig:Combobox
        id="package-form"
        name="package"
        placeholder="Select package..."
        searchPlaceholder="Search packages..."
        :choices="packages"
        :required="true"
    />
    <button
        type="submit"
        class="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow-xs hover:bg-primary/90"
    >Submit</button>
</form>

Disabled

Loading...
{% set packages = [
    {value: 'turbo', label: 'UX Turbo'},
    {value: 'twig-component', label: 'UX Twig Component'},
    {value: 'live-component', label: 'UX Live Component'},
] %}
<twig:Combobox
    id="package-disabled"
    placeholder="Select package..."
    searchPlaceholder="Search packages..."
    :choices="packages"
    :disabled="true"
/>

Clearable

Loading...
{% set packages = [
    {value: 'turbo', label: 'UX Turbo'},
    {value: 'twig-component', label: 'UX Twig Component'},
    {value: 'live-component', label: 'UX Live Component'},
    {value: 'autocomplete', label: 'UX Autocomplete'},
    {value: 'icons', label: 'UX Icons'},
] %}
<twig:Combobox
    id="package-clearable"
    value="live-component"
    placeholder="Select package..."
    searchPlaceholder="Search packages..."
    clearable
    :choices="packages"
/>

With Groups

Loading...
{% set packages = [
    {label: 'Stimulus', choices: [
        {value: 'twig-component', label: 'UX Twig Component'},
        {value: 'live-component', label: 'UX Live Component'},
        {value: 'autocomplete', label: 'UX Autocomplete'},
        {value: 'turbo', label: 'UX Turbo'},
    ]},
    {label: 'Frontend Frameworks', choices: [
        {value: 'react', label: 'UX React'},
        {value: 'vue', label: 'UX Vue'},
        {value: 'svelte', label: 'UX Svelte'},
    ]},
    {label: 'UI & Visualization', choices: [
        {value: 'icons', label: 'UX Icons'},
        {value: 'chartjs', label: 'UX Chart.js'},
        {value: 'map', label: 'UX Map'},
    ]},
] %}
<twig:Combobox
    id="package-grouped"
    placeholder="Select package..."
    searchPlaceholder="Search packages..."
    :choices="packages"
/>

Empty State

Loading...
{% set frameworks = [
    {value: 'next', label: 'Next.js'},
    {value: 'sveltekit', label: 'SvelteKit'},
] %}
<twig:Combobox
    id="framework-empty"
    placeholder="Select framework..."
    searchPlaceholder="Search framework..."
    emptyMessage="No frameworks found. Try a different search."
    :choices="frameworks"
/>

Long List

Loading...
{% set languages = [
    {value: 'php', label: 'PHP'},
    {value: 'javascript', label: 'JavaScript'},
    {value: 'typescript', label: 'TypeScript'},
    {value: 'python', label: 'Python'},
    {value: 'ruby', label: 'Ruby'},
    {value: 'go', label: 'Go'},
    {value: 'rust', label: 'Rust'},
    {value: 'java', label: 'Java'},
    {value: 'kotlin', label: 'Kotlin'},
    {value: 'swift', label: 'Swift'},
    {value: 'csharp', label: 'C#'},
    {value: 'cpp', label: 'C++'},
    {value: 'c', label: 'C'},
    {value: 'scala', label: 'Scala'},
    {value: 'elixir', label: 'Elixir'},
    {value: 'erlang', label: 'Erlang'},
    {value: 'haskell', label: 'Haskell'},
    {value: 'ocaml', label: 'OCaml'},
    {value: 'fsharp', label: 'F#'},
    {value: 'clojure', label: 'Clojure'},
    {value: 'dart', label: 'Dart'},
    {value: 'r', label: 'R'},
    {value: 'matlab', label: 'MATLAB'},
    {value: 'perl', label: 'Perl'},
    {value: 'lua', label: 'Lua'},
    {value: 'julia', label: 'Julia'},
    {value: 'groovy', label: 'Groovy'},
    {value: 'shell', label: 'Shell'},
    {value: 'powershell', label: 'PowerShell'},
    {value: 'sql', label: 'SQL'},
    {value: 'html', label: 'HTML'},
    {value: 'css', label: 'CSS'},
    {value: 'sass', label: 'Sass'},
    {value: 'less', label: 'Less'},
    {value: 'graphql', label: 'GraphQL'},
    {value: 'yaml', label: 'YAML'},
    {value: 'toml', label: 'TOML'},
    {value: 'json', label: 'JSON'},
    {value: 'xml', label: 'XML'},
    {value: 'markdown', label: 'Markdown'},
    {value: 'latex', label: 'LaTeX'},
    {value: 'zig', label: 'Zig'},
    {value: 'nim', label: 'Nim'},
    {value: 'crystal', label: 'Crystal'},
    {value: 'reason', label: 'ReasonML'},
    {value: 'elm', label: 'Elm'},
    {value: 'purescript', label: 'PureScript'},
    {value: 'coffeescript', label: 'CoffeeScript'},
    {value: 'objective-c', label: 'Objective-C'},
    {value: 'vba', label: 'VBA'},
    {value: 'cobol', label: 'COBOL'},
] %}
<twig:Combobox
    id="language"
    placeholder="Select language..."
    searchPlaceholder="Search languages..."
    :choices="languages"
/>

API Reference

<twig:Combobox>

Prop Type Default
id 
string -
name 
string|null null
value 
string ''
choices 
array []
placeholder 
string 'Select option...'
searchPlaceholder 
string 'Search...'
emptyMessage 
string 'No results found.'
disabled 
boolean false
required 
boolean false
clearable 
boolean false