vue-select is abandoned. Here's what I built to replace it.

Nemanja Malesija

Nemanja Malesija

2026-02-26T07:58:11Z

5 min read

If you've worked with Vue, chances are you came across vue-select. With over 4.6 million weekly downloads, it became the default select component for the Vue ecosystem. It handled filtering, multi-select, tagging, and async search out of the box. For most of Vue 2's lifetime, it just worked.

Then Vue 3 arrived, and vue-select didn't follow. The v4 beta landed in November 2022 targeting Vue 3, but it never reached a stable release. Over two years later, the beta is still the latest version. Issues pile up. PRs go unreviewed. Thousands of projects are stuck on a component that's effectively abandoned.

I don't say this to criticize the maintainer -- maintaining a popular open source project solo is brutally hard, and vue-select served the community well for years. But the reality is clear: if you're building a new Vue 3 app today, vue-select is not a reliable choice.

The pain points

If you've tried to use vue-select with Vue 3, you've probably hit these:

  • No stable Vue 3 release. The v4 beta works for basic cases but has unresolved issues with reactivity, slots, and TypeScript support.
  • Opinionated CSS that fights your design system. vue-select ships over 400 lines of CSS. If your project uses Tailwind, a custom design system, or anything beyond the default look, you're overriding styles constantly.
  • No TypeScript generics. In a world where ref<User>() and defineProps<{ items: User[] }>() are standard, vue-select's prop types are loose at best.

Introducing vue-superselect

I built vue-superselect to solve all three. It's a headless, accessible, TypeScript-first select/combobox component for Vue 3.

npm install vue-superselect
Enter fullscreen mode Exit fullscreen mode

Headless means it ships zero CSS. You bring your own styles -- classes, Tailwind, CSS-in-JS, whatever your project uses. No more fighting a component's built-in stylesheet.

<script setup lang="ts">
import { ref } from 'vue'
import {
  SelectRoot,
  SelectControl,
  SelectInput,
  SelectContent,
  SelectOption,
} from 'vue-superselect'

const selected = ref<string | null>(null)
const fruits = ['Apple', 'Banana', 'Cherry', 'Grape', 'Orange']
</script>

<template>
  <SelectRoot v-model="selected">
    <SelectControl>
      <SelectInput placeholder="Pick a fruit..." />
    </SelectControl>
    <SelectContent>
      <SelectOption
        v-for="fruit in fruits"
        :key="fruit"
        :value="fruit"
        :label="fruit"
      >
        {{ fruit }}
      </SelectOption>
    </SelectContent>
  </SelectRoot>
</template>
Enter fullscreen mode Exit fullscreen mode

Two APIs, one library

vue-superselect gives you two ways to build selects, depending on how much control you need.

Compound components are the primary API. They use Vue's provide/inject to share state, so you compose them declaratively in your template. This is the approach most projects should start with -- it's concise, readable, and handles all the ARIA attributes automatically.

The composable API (useSelect()) is for when you need full control over the rendered DOM. Instead of components, you get prop getter functions that return the right attributes for each element. You spread them onto your own <input>, <ul>, and <li> elements with v-bind. This is useful for design system integrations, custom animations, or when you want to render something the compound components don't support.

<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import { useSelect } from 'vue-superselect'
import type { CollectionItem } from 'vue-superselect'

const fruits = ['Apple', 'Banana', 'Cherry', 'Grape', 'Orange']

const {
  getRootProps,
  getInputProps,
  getListboxProps,
  getOptionProps,
  isOpen,
  value,
  visibleItems,
  registerItem,
  unregisterItem,
} = useSelect<string>()

const items: CollectionItem<string>[] = fruits.map((fruit, i) => ({
  id: `fruit-${i}`,
  value: fruit,
  label: fruit,
  disabled: false,
  element: null,
}))

onMounted(() => items.forEach(registerItem))
onBeforeUnmount(() => items.forEach((item) => unregisterItem(item.id)))
</script>

<template>
  <div v-bind="getRootProps()">
    <input v-bind="getInputProps({ placeholder: 'Search fruits...' })" />
    <ul v-if="isOpen" v-bind="getListboxProps()">
      <li
        v-for="item in visibleItems"
        :key="item.id"
        v-bind="getOptionProps(item)"
      >
        {{ item.label }}
      </li>
    </ul>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Multi-select with tags

Multi-select is a first-class feature. Set multiple on SelectRoot and the v-model becomes an array. Add SelectTag for removable tags, and hide-selected to filter out already-chosen options from the dropdown.

<script setup lang="ts">
import { ref } from 'vue'
import {
  SelectRoot,
  SelectControl,
  SelectInput,
  SelectContent,
  SelectOption,
  SelectTag,
} from 'vue-superselect'

const selected = ref<string[]>([])
const skills = ['Vue.js', 'TypeScript', 'React', 'Node.js', 'Python', 'Go']
</script>

<template>
  <SelectRoot v-model="selected" multiple hide-selected>
    <SelectControl v-slot="{ selectedItems, removeItem }">
      <SelectTag
        v-for="item in selectedItems"
        :key="String(item.value)"
        :value="item.value"
        :label="item.label"
        @remove="removeItem"
      />
      <SelectInput placeholder="Select skills..." />
    </SelectControl>
    <SelectContent>
      <SelectOption
        v-for="skill in skills"
        :key="skill"
        :value="skill"
        :label="skill"
      >
        {{ skill }}
      </SelectOption>
    </SelectContent>
  </SelectRoot>
</template>
Enter fullscreen mode Exit fullscreen mode

TypeScript all the way down

vue-superselect is built in TypeScript strict mode with full generic inference. When you pass typed items, the v-model value, slot props, and event payloads are all correctly typed. No any, no manual casting.

interface User {
  id: number
  name: string
  role: 'admin' | 'member'
}

// v-model is correctly typed as number | null
// SelectOption slot props know about User
Enter fullscreen mode Exit fullscreen mode

What else?

The full feature list:

  • Accessible by default -- WAI-ARIA combobox pattern, full keyboard navigation, screen reader announcements via a live region
  • Filtering -- built-in case-insensitive search, custom filter functions, configurable debounce, IME-safe
  • Dropdown positioning -- optional @floating-ui/vue integration for auto-flip and teleport support
  • Tree-shakeable -- sideEffects: false, ESM + CJS dual builds, /*#__PURE__*/ annotations on all components
  • Small -- under 12 kB gzipped for the full library

Coming from vue-select?

There's a migration guide in the docs with a prop mapping table and before/after code examples. The core concepts map naturally: v-model, options, label, reduce all have direct equivalents. The main shift is from built-in styles to bringing your own.

Alternatives

I want to be upfront: this isn't the only option. Radix Vue includes a combobox component, and Headless UI has a listbox. Both are excellent libraries.

vue-superselect is purpose-built for the select/combobox pattern specifically. It's a single-purpose library with a dedicated API for filtering, multi-select with tags, and combobox behaviors. If you're already using Radix Vue or Headless UI in your project, their built-in components might be all you need. If you want a focused solution for the select use case -- especially if you're migrating from vue-select -- this was built for you.

Links

I'd love to hear what you think. If you run into issues, please open a GitHub issue -- I'm actively maintaining this and plan to ship Option Groups, Form Integration, and Async Data Loading in the next releases.

GitHub logo nemanjamalesija / vue-superselect

Headless, accessible, TypeScript-first select/combobox for Vue 3

vue-superselect

Headless, accessible, TypeScript-first select/combobox for Vue 3.

Zero runtime CSS. Full keyboard navigation. WAI-ARIA combobox pattern. Dual API: compound components or useSelect() composable.

Documentation

Install

npm install vue-superselect
Enter fullscreen mode Exit fullscreen mode

Vue 3.5+ required. @floating-ui/vue is an optional peer dependency for smart dropdown positioning.

Quick Start

<script setup lang="ts"&gt
import { ref } from 'vue'
import {
  SelectRoot,
  SelectControl,
  SelectInput,
  SelectContent,
  SelectOption,
} from 'vue-superselect'

const selected = ref<string | null>(null)
const fruits = ['Apple', 'Banana', 'Cherry', 'Grape', 'Orange']
</script>

<template>
  <SelectRoot v-model="selected">
    <SelectControl>
      <SelectInput placeholder="Pick a fruit..." aria-label="Fruit" />
    </SelectControl>
    <SelectContent>
      <SelectOption
        v-for=
Enter fullscreen mode Exit fullscreen mode