Combobox

Choose from a list of suggested values with full keyboard support.

<script setup lang="ts">
import type { CheckAnimation } from '@animate-ui/vue/provider'
import { Tick02Icon, UnfoldMoreIcon } from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/vue'
import { AnimateUIProvider } from '@animate-ui/vue/provider'
import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import {
  Combobox,
  ComboboxAnchor,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
} from '@/components/ui/combobox'

withDefaults(defineProps<{ checkAnimation?: CheckAnimation }>(), {
  checkAnimation: 'draw',
})

const frameworks = [
  { value: 'next.js', label: 'Next.js' },
  { value: 'sveltekit', label: 'SvelteKit' },
  { value: 'nuxt.js', label: 'Nuxt.js' },
  { value: 'remix', label: 'Remix' },
  { value: 'astro', label: 'Astro' },
]

const selectedFramework = ref<(typeof frameworks)[number]>()
</script>

<template>
  <AnimateUIProvider :check-animation="checkAnimation">
    <Combobox v-model="selectedFramework" by="label">
      <ComboboxAnchor as-child>
        <ComboboxTrigger as-child>
          <Button variant="outline" class="w-[200px] justify-between">
            <ComboboxValue>{{ selectedFramework?.label ?? 'Select framework...' }}</ComboboxValue>
            <HugeiconsIcon :icon="UnfoldMoreIcon" :stroke-width="2" class="opacity-50" />
          </Button>
        </ComboboxTrigger>
      </ComboboxAnchor>

      <ComboboxList>
        <ComboboxInput placeholder="Search framework..." />
        <ComboboxEmpty>No framework found.</ComboboxEmpty>
        <ComboboxGroup>
          <ComboboxItem
            v-for="framework in frameworks"
            :key="framework.value"
            :value="framework"
          >
            {{ framework.label }}
            <ComboboxItemIndicator>
              <HugeiconsIcon :icon="Tick02Icon" :stroke-width="2" />
            </ComboboxItemIndicator>
          </ComboboxItem>
        </ComboboxGroup>
      </ComboboxList>
    </Combobox>
  </AnimateUIProvider>
</template>

Installation

Install the packages

npm install @animate-ui/vue motion-v
pnpm add @animate-ui/vue motion-v
yarn add @animate-ui/vue motion-v
bun add @animate-ui/vue motion-v

Add the shadcn-vue component

npx shadcn-vue@latest add combobox
pnpm dlx shadcn-vue@latest add combobox
yarn dlx shadcn-vue@latest add combobox
bun x shadcn-vue@latest add combobox

Animate it

The CLI finds your shadcn-vue components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate combobox
pnpm dlx @animate-ui/cli@latest animate combobox
yarn dlx @animate-ui/cli@latest animate combobox
bun x @animate-ui/cli@latest animate combobox

Usage

<script setup lang="ts">
import {
  ComboboxAnchor,
  ComboboxContent,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxPortal,
  ComboboxRoot,
  ComboboxViewport,
} from '@animate-ui/vue/primitives/reka-ui';
</script>

<template>
  <ComboboxRoot>
    <ComboboxAnchor>
      <ComboboxInput placeholder="Search a framework…" />
    </ComboboxAnchor>
    <ComboboxPortal>
      <ComboboxContent position="popper" :side-offset="4">
        <ComboboxViewport>
          <ComboboxItem value="nuxt">
            Nuxt
            <ComboboxItemIndicator>✓</ComboboxItemIndicator>
          </ComboboxItem>
          <ComboboxItem value="astro">
            Astro
            <ComboboxItemIndicator>✓</ComboboxItemIndicator>
          </ComboboxItem>
        </ComboboxViewport>
      </ComboboxContent>
    </ComboboxPortal>
  </ComboboxRoot>
</template>

When a trigger button shows the choice, wrap its label in ComboboxValue so the text swaps as it changes:

<ComboboxTrigger as-child>
  <Button variant="outline">
    <ComboboxValue>{{ selected?.label ?? 'Select framework...' }}</ComboboxValue>
  </Button>
</ComboboxTrigger>

Check animation

The chosen item's check draws itself while the previous one erases itself. With animation="pop" on every item's ComboboxItemIndicator, the check glides from the previous item to the chosen one instead (a first choice pops it in from half its size).

The same demo with check-animation="pop" (its Controls switch it back to draw):

To pop every check of your app at once (checkboxes, menus, selects, comboboxes), set check-animation="pop" on an AnimateUIProvider instead.

Animation

The motion of Animate UI's React Combobox:

  • ComboboxContent with position="popper" fades and scales in from 0.9, from its anchor (--reka-combobox-content-transform-origin), in 220ms ({ duration: 0.22, ease: [0.215, 0.61, 0.355, 1] }, a soft ease-out), and closes the same way before it unmounts. Laid inline (reka-ui's default position), it only fades. Reopening while it closes picks up from where it is.
  • Stagger: opening from closed, the options (and labels, separators) come in one after another, 33ms apart (closer past 10 rows: 33ms × √(10 / rows), so a long list doesn't wait): each slides 8px in from the right and clears from a 4px blur as it fades in, in 0.3s on a soft ease-out ({ duration: 0.3, ease: [0.215, 0.61, 0.355, 1] }). The box itself is there at once (its opacity in 0.1s, its scale as before), so the rows bring the fade, and a disabled row comes in at its own opacity. Filtering as the user types doesn't stagger again, closing shows every row at once, and under reduced motion nothing staggers.
  • ComboboxValue (a span around the label a trigger button shows): when the choice changes, the old text rises 6px into a 4px blur and fades, while the new one rises 6px into place out of the same blur, in 400ms on a soft ease-out ({ duration: 0.4, ease: [0.215, 0.61, 0.355, 1] }), instead of changing in one frame. The trigger keeps its size.
  • The highlight: one surface glides behind the highlighted item on a spring (stiffness 500, damping 40), following the pointer and the arrow keys alike. Moving to another item, it carries on from where it is, at its speed: however fast the pointer or the arrow keys go, there's one highlight on screen. It only fades in (200ms, on CSS's ease) when it appears from nothing, and its color changes the same way when the item's highlighted background differs from the previous one's. It glides on the compositor (a Web Animation of transform), so a busy main thread doesn't stall it. It wears the item's own highlighted background (data-[highlighted]:bg-accent…), read from your styles: the item's background is cleared while it's highlighted; its text colors stay.
  • Filtering while typing is instant: items appear and disappear on the next frame, with no per-item or height animation.
  • ComboboxItemIndicator (kept mounted on every item, so a check can hide itself): with a single value, the chosen item's check draws itself like a Checkbox's check (its stroke and opacity in 0.3s on Motion's easeOut, at once) while the previous one erases itself, and the list holds still for 250ms before it closes, so the change is seen nearly done before the list fades out. With animation="pop" (or check-animation="pop" on an AnimateUIProvider), the check glides there instead, on the highlight's spring (stiffness 500, damping 40), and the list holds for 150ms, and a first choice pops it in from half its size out of a 2px blur (a spring, stiffness 500, damping 24). The demo's Controls switch between the two. Choosing the item already chosen, and closing any other way (Escape, a click outside), don't wait. With multiple, a selected item's check draws itself, and erases itself when it's deselected; with pop, it pops in from half its size and shrinks back out in 0.15s. Only a change of selection animates the checks: opening the list shows them at rest.
  • A closing list is inert, and a press on its trigger while it leaves reopens it.
  • Open from the start (default-open), the list plays its entrance on page load.
  • Under reduced motion, nothing scales or glides: the list and checks only fade, and the list doesn't hold; the highlight jumps from item to item and only fades in when it appears.

API Reference

Every part takes exactly the props of reka-ui's Combobox. Animate UI adds the animation props below.

ComboboxRoot, ComboboxAnchor, ComboboxInput, ComboboxTrigger, ComboboxCancel, ComboboxPortal, ComboboxViewport, ComboboxEmpty, ComboboxGroup, ComboboxLabel, ComboboxSeparator, ComboboxArrow, ComboboxVirtualizer

reka-ui props only. They are not animated.

ComboboxValue

A span (as and as-child work, like reka-ui's Primitive) around the label a trigger shows: its text swaps as it changes.

ComboboxItem

reka-ui props only. It takes part in the highlight; it needs nothing more.

ComboboxContent

reka-ui props, plus:

PropTypeDefault
transition?
Transition{ duration: 0.22, ease: [0.215, 0.61, 0.355, 1] }
exit-transition?
Transitiontransition

ComboboxItemIndicator

reka-ui props, plus:

PropTypeDefault
animation?
'draw' | 'pop'the closest AnimateUIProvider's check-animation, else 'draw'
transition?
Transitiondraw: 0.3s on Motion's easeOut both ways (a Checkbox's, without its delay); pop: a spring (stiffness 500, damping 24) in, 0.15s out, and the highlight's spring (stiffness 500, damping 40) for the glide

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/23/2026