Phi
Combobox
@dicehub/phiv1.0.0-beta.1

Combobox

A searchable select component for filtering and choosing options.

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref(["apple"]);
const fruits = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
];

const collection = createComboboxCollection({
  items: fruits,
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox v-model="value" :collection="collection">
    <Combobox.TriggerInput placeholder="Please select" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">{{ item.label }}</Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Installation

Barrel

import { Combobox } from "@dicehub/phi";

Granular

import { Combobox } from "@dicehub/phi/components/combobox";

Usage

Standard Combobox.TriggerInput, Combobox.TriggerValue, andCombobox.TriggerMultipleWithInput triggers show the same up/down double chevron as Select. Replace it with your own control through as-child, or through thetrigger slot on Combobox.TriggerInput, when you need a custom indicator.

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref(["banana"]);
const fruits = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
];

const collection = createComboboxCollection({
  items: fruits,
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox v-model="value" :collection="collection">
    <Combobox.TriggerInput placeholder="Select a fruit" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">{{ item.label }}</Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Examples

Sizes

Trigger Input

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref(["apple"]);
const collection = createComboboxCollection({
  items: [{ label: "Apple", value: "apple" }, { label: "Banana", value: "banana" }],
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <div class="grid gap-3">
    <Combobox v-model="value" :collection="collection" size="sm">
      <Combobox.TriggerInput placeholder="Small" />
      <Combobox.Content>
        <Combobox.Empty />
        <Combobox.List>
          <template #default="{ item }">
            <Combobox.Item :item="item">{{ item.label }}</Combobox.Item>
          </template>
        </Combobox.List>
      </Combobox.Content>
    </Combobox>
    <Combobox v-model="value" :collection="collection">
      <Combobox.TriggerInput placeholder="Base" />
      <Combobox.Content>
        <Combobox.Empty />
        <Combobox.List>
          <template #default="{ item }">
            <Combobox.Item :item="item">{{ item.label }}</Combobox.Item>
          </template>
        </Combobox.List>
      </Combobox.Content>
    </Combobox>
  </div>
</template>

Trigger Value

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref(["en"]);
const collection = createComboboxCollection({
  items: [
    { emoji: "🇬🇧", label: "English", value: "en" },
    { emoji: "🇫🇷", label: "French", value: "fr" },
    { emoji: "🇩🇪", label: "German", value: "de" },
  ],
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <div class="grid gap-3">
    <Combobox v-model="value" :collection="collection" size="sm">
      <Combobox.TriggerValue placeholder="Small" />
      <Combobox.Content>
        <Combobox.Input placeholder="Search" />
        <Combobox.Empty />
        <Combobox.List>
          <template #default="{ item }">
            <Combobox.Item :item="item">{{ item.emoji }} {{ item.label }}</Combobox.Item>
          </template>
        </Combobox.List>
      </Combobox.Content>
    </Combobox>
    <Combobox v-model="value" :collection="collection">
      <Combobox.TriggerValue placeholder="Base" />
      <Combobox.Content>
        <Combobox.Input placeholder="Search" />
        <Combobox.Empty />
        <Combobox.List>
          <template #default="{ item }">
            <Combobox.Item :item="item">{{ item.emoji }} {{ item.label }}</Combobox.Item>
          </template>
        </Combobox.List>
      </Combobox.Content>
    </Combobox>
  </div>
</template>

Searchable Item Inside

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref(["en"]);
const languages = [
  { emoji: "🇬🇧", label: "English", value: "en" },
  { emoji: "🇫🇷", label: "French", value: "fr" },
  { emoji: "🇩🇪", label: "German", value: "de" },
];

const collection = createComboboxCollection({
  items: languages,
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox v-model="value" :collection="collection">
    <Combobox.TriggerValue placeholder="Select a language" />
    <Combobox.Content>
      <Combobox.Input placeholder="Search languages" />
      <Combobox.Empty />
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">{{ item.emoji }} {{ item.label }}</Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Searchable Select with Placeholder

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref([]);
const languages = [
  { emoji: "🇬🇧", label: "English", value: "en" },
  { emoji: "🇫🇷", label: "French", value: "fr" },
  { emoji: "🇩🇪", label: "German", value: "de" },
];

const collection = createComboboxCollection({
  items: languages,
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox v-model="value" :collection="collection">
    <Combobox.TriggerValue placeholder="Select a language" />
    <Combobox.Content>
      <Combobox.Input placeholder="Search languages" />
      <Combobox.Empty />
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">{{ item.emoji }} {{ item.label }}</Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Custom Trigger

<script setup>
import { ref } from "vue";
import { PhCaretUpDown } from "@phosphor-icons/vue";
import { Button } from "@dicehub/phi/components/button";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref(["en"]);
const collection = createComboboxCollection({
  items: [
    { emoji: "🇬🇧", label: "English", value: "en" },
    { emoji: "🇫🇷", label: "French", value: "fr" },
    { emoji: "🇩🇪", label: "German", value: "de" },
  ],
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox v-model="value" :collection="collection">
    <Combobox.Trigger as-child>
      <Button variant="ghost" size="sm">
        <Combobox.Value v-slot="{ items }" placeholder="Select language">
          <span>{{ items[0]?.emoji }} {{ items[0]?.label }}</span>
        </Combobox.Value>
        <PhCaretUpDown :size="14" aria-hidden="true" />
      </Button>
    </Combobox.Trigger>
    <Combobox.Content>
      <Combobox.Input placeholder="Search languages" />
      <Combobox.Empty />
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">{{ item.emoji }} {{ item.label }}</Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Grouped

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref(["amsterdam"]);
const groups = [
  { label: "Asia", items: [{ label: "Tokyo", value: "tokyo" }] },
  { label: "Europe", items: [{ label: "Amsterdam", value: "amsterdam" }] },
];

const collection = createComboboxCollection({
  items: groups.flatMap((group) => group.items),
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox v-model="value" :collection="collection">
    <Combobox.TriggerInput placeholder="Select server" />
    <Combobox.Content>
      <Combobox.List :render-items="false">
        <Combobox.Group v-for="group in groups" :key="group.label">
          <Combobox.GroupLabel>{{ group.label }}</Combobox.GroupLabel>
          <Combobox.Item v-for="server in group.items" :key="server.value" :item="server">
            {{ server.label }}
          </Combobox.Item>
        </Combobox.Group>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Multiple

<script setup>
import { ref } from "vue";
import { Button } from "@dicehub/phi/components/button";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref([]);
const bots = [
  { label: "Googlebot", value: "googlebot", author: "Google" },
  { label: "Bingbot", value: "bingbot", author: "Microsoft" },
  { label: "DuckDuckBot", value: "duckduckbot", author: "DuckDuckGo" },
];

const collection = createComboboxCollection({
  items: bots,
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <div class="flex items-start gap-3">
    <Combobox v-model="value" :collection="collection" multiple>
      <Combobox.TriggerMultipleWithInput placeholder="Select bots" />
      <Combobox.Content>
        <Combobox.Empty />
        <Combobox.List>
          <template #default="{ item }">
            <Combobox.Item :item="item">
              <span>{{ item.label }}</span>
              <span>{{ item.author }}</span>
            </Combobox.Item>
          </template>
        </Combobox.List>
      </Combobox.Content>
    </Combobox>
    <Button variant="primary">Submit</Button>
  </div>
</template>

With Field

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref([]);
const collection = createComboboxCollection({
  items: [
    { label: "PostgreSQL", value: "postgresql" },
    { label: "MySQL", value: "mysql" },
    { label: "Redis", value: "redis" },
  ],
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox
    v-model="value"
    :collection="collection"
    label="Database"
    description="Select your preferred database"
  >
    <Combobox.TriggerInput placeholder="Select database" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">{{ item.label }}</Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Disabled

<script setup>
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const fruits = createComboboxCollection({
  items: [{ label: "Apple", value: "apple" }, { label: "Banana", value: "banana" }],
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
const languages = createComboboxCollection({
  items: [{ emoji: "🇬🇧", label: "English", value: "en" }, { emoji: "🇫🇷", label: "French", value: "fr" }],
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <div class="flex flex-wrap gap-4">
    <Combobox :collection="fruits" :default-value="['apple']" disabled>
      <Combobox.TriggerInput placeholder="Select fruit" />
    </Combobox>
    <Combobox :collection="languages" :default-value="['en']" disabled>
      <Combobox.TriggerValue />
    </Combobox>
  </div>
</template>

Disabled Items

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref([]);
const databases = [
  { label: "PostgreSQL", value: "postgresql" },
  { label: "MySQL", value: "mysql" },
  { label: "MariaDB", value: "mariadb", disabled: true, note: "Beta" },
  { label: "Apache Cassandra", value: "cassandra", disabled: true, note: "Coming soon" },
];

const collection = createComboboxCollection({
  items: databases,
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
  isItemDisabled: (item) => item.disabled === true,
});
</script>

<template>
  <Combobox v-model="value" :collection="collection">
    <Combobox.TriggerInput placeholder="Select database" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">
            {{ item.label }}<span v-if="item.note"> - {{ item.note }}</span>
          </Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Error State

<script setup>
import { ref } from "vue";
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const value = ref([]);
const collection = createComboboxCollection({
  items: [
    { label: "PostgreSQL", value: "postgresql" },
    { label: "MySQL", value: "mysql" },
    { label: "Redis", value: "redis" },
  ],
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox
    v-model="value"
    :collection="collection"
    label="Database"
    error="Please select a database."
  >
    <Combobox.TriggerInput placeholder="Select database" />
    <Combobox.Content>
      <Combobox.Empty />
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">{{ item.label }}</Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Positioning and Dropdown Size

Pass Ark positioning options directly to Combobox.Content. Content values override matchingCombobox.Root positioning values. Use class or style for dimensions.

<script setup>
import { Combobox, createComboboxCollection } from "@dicehub/phi/components/combobox";

const collection = createComboboxCollection({
  items: [
    { label: "Apple", value: "apple" },
    { label: "Apricot", value: "apricot" },
    { label: "Avocado", value: "avocado" },
    { label: "Banana", value: "banana" },
    { label: "Blueberry", value: "blueberry" },
  ],
  itemToString: (item) => item.label,
  itemToValue: (item) => item.value,
});
</script>

<template>
  <Combobox :collection="collection">
    <Combobox.TriggerInput placeholder="Select a fruit" />
    <Combobox.Content
      class="max-h-48"
      placement="bottom-start"
      strategy="fixed"
      :flip="false"
      :overflow-padding="12"
    >
      <Combobox.List>
        <template #default="{ item }">
          <Combobox.Item :item="item">{{ item.label }}</Combobox.Item>
        </template>
      </Combobox.List>
    </Combobox.Content>
  </Combobox>
</template>

Sub-components

ComponentDescription
Combobox.RootManages value, filtering, open state, and field state.
Combobox.TriggerInputSearchable input trigger with clear and menu controls. Set as-child to forward input behavior to one custom control.
Combobox.TriggerValueButton-like value trigger. Set as-child to forward trigger behavior to one custom control.
Combobox.TriggerMultipleWithInputMulti-select trigger with removable chips and inline search.
Combobox.ContentPositioned dropdown panel. Accepts all Ark positioning options.
Combobox.InputSearch input flush with the dropdown top and side edges, with square bottom corners.
Combobox.ListListbox container. Can render collection items automatically.
Combobox.ItemSelectable option with selected indicator.
Combobox.GroupGroups related options.
Combobox.GroupLabelAccessible group heading.
Combobox.ChipSelected item chip used by the multi-select trigger.
Combobox.EmptyFallback content when the filtered collection is empty.

API Reference

Combobox.Root

PropTypeDefaultDescription
collectionListCollection-Ark collection used for labels, values, disabled items, and filtering.
itemsunknown[][]Convenience input for creating a collection inside Phi.
modelValuestring[]-Controlled selected values. Use v-model in Vue.
defaultValuestring[][]Initial selected values for uncontrolled usage.
multiplebooleanfalseAllows more than one selected value.
size"xs" | "sm" | "base" | "lg""base"Sets trigger height, padding, and font size.
labelstring-Optional field label.
descriptionstring-Optional helper text.
errorstring-Shows error text and marks the field invalid.
disabledbooleanfalseDisables input and trigger interactions.
closeOnSelectbooleantrueCloses the dropdown after selecting a single item.
positioningComboboxPositioningOptions{ placement: "bottom-start", gutter: 4 }Base Ark positioning options. Direct Content props override matching values.

Combobox.Item

PropTypeDefaultDescription
itemunknown-Collection item rendered as an option.
valueunknown-Fallback item prop for simple values.

Combobox.TriggerInput

PropTypeDefaultDescription
asChildbooleanfalseMerges Ark UI input behavior into the single default-slot child.
default slotcomponent-Custom input control used when `asChild` is true.

Combobox.TriggerValue

PropTypeDefaultDescription
asChildbooleanfalseMerges Ark UI trigger behavior into the single default-slot child.
default slot{ items; label; value }-Value state for the default trigger or one custom trigger child.

Combobox.Content

PropTypeDefaultDescription
classstring-Additional classes for dropdown width, height, and appearance.
arrowPaddingnumberRoot valueMinimum space between an arrow and a content corner.
boundaryBoundary | (() => Boundary) | "clipping-ancestors"Root valueBoundary used to detect popup overflow.
fitViewportbooleanRoot valueLimits the popup to the available viewport size.
flipboolean | Placement[]Root valueChanges placement when the preferred side has no space.
getAnchorElement() => HTMLElement | VirtualElement | nullRoot valueReturns a custom anchor element for the popup.
getAnchorRect(element) => AnchorRect | nullRoot valueDeprecated anchor rectangle callback. Use getAnchorElement.
gutternumber4Gap between the anchor and popup on the main axis.
hideWhenDetachedbooleanRoot valueHides the popup when its anchor is detached.
listenersboolean | AutoUpdateOptionsRoot valueControls automatic position update listeners.
offset{ mainAxis?: number; crossAxis?: number }Root valueAdds main-axis and cross-axis offsets.
onComplete(data: ComputePositionReturn) => voidRoot valueRuns after Floating UI computes a position.
onPositioned(data: { placed: boolean }) => voidRoot valueRuns when the popup placement state changes.
overflowPaddingnumberRoot valueKeeps this space between the popup and overflow boundary.
overlapbooleanRoot valueAllows the popup to overlap its anchor.
placementPlacement"bottom-start"Sets the preferred side and alignment.
restoreStylesbooleanRoot valueRestores positioning styles during cleanup.
sameWidthbooleanRoot valueMatches the popup width to the anchor width.
shiftnumberRoot valueMoves the popup along the alignment axis when it overflows.
sizeMiddlewarebooleanRoot valueControls Floating UI size calculations.
slidebooleanRoot valueSlides the popup to keep it inside the boundary.
strategy"absolute" | "fixed"Root valueSets the CSS positioning strategy.
updatePosition({ updatePosition, floatingElement }) => voidRoot valueReceives a callback for manual position updates.