Vue 3 Select Component

website·Indexed 29 days ago

https://vue3-select-component.vercel.app

An accessible select component for Vue 3 (3.5+) focusing on developer experience, type safety, and ease of use. It provides both an assembled Select for quick implementation and headless primitives for custom composition. Features include single and multi-select modes, WAI-ARIA Combobox Pattern compliance, TypeScript support, and compatibility with Nuxt 3 and 4.

Tokens
20.9K
Snippets
99
Records
139
Agent score
74%

What's inside Vue 3 Select Component

  1. Understand the vue3-select-component Primitives architecture

    Primitives are headless building blocks that share state via SelectRoot using provide/inject. All primitives must be nested inside a SelectRoot to function. The hierarchy is as follows:

    • SelectRoot (State + Context Provider)
      • SelectTrigger
        • SelectIcon
        • SelectValue
        • SelectInput (used when searchable)
        • SelectTrailingIcon
        • SelectClear
      • SelectPopover
        • SelectListbox
          • SelectNoOptions
          • SelectOption (xN)
            • SelectGroup (Optional)
            • SelectGroupLabel (Optional)
            • SelectSeparator (Optional)
  2. Understand the assembled Select component architecture

    The assembled Select is a fixed composition of primitives. It uses SelectRoot as the state owner, which provides a SelectContext injected into child primitives. The component tree consists of SelectRoot containing a SelectTrigger (with optional SelectIcon, SelectValue, SelectInput, SelectTrailingIcon, and SelectClear) and a SelectPopover containing a SelectListbox (with SelectNoOptions, SelectOption entries, and an optional SelectCreateItem).
  3. Migrate from vue3-select-component v0 to v1

    v1 is a breaking release that introduces a new architecture. While v0 used a single monolithic component, v1 provides an assembled Select component for common use cases and a set of headless primitives for full customization. v0 prop aliases are not supported in v1; all props must be renamed to their v1 equivalents.
  4. Accessibility overview of Vue 3 Select Component

    The Vue 3 Select Component follows the WAI-ARIA Combobox Pattern. It provides built-in support for semantic ARIA attributes (combobox, listbox, option), full keyboard navigation, focus management using aria-activedescendant for searchable inputs, and screen reader support via descriptive labels and state attributes.
  5. Update imports for v1 and Primitives

    Update the main component import and add imports for the new primitives if needed. The bundled CSS import remains the same for the assembled Select component.
    // v1 Assembled Component
    import Select from "vue3-select-component"
    import "vue3-select-component/styles"
    
    // v1 Primitives (New)
    import { SelectRoot, SelectTrigger } from "vue3-select-component/primitives"
  6. Search behavior and focus management

    When searchable is enabled, typing filters options in real time and the active option syncs to the first matching result. Focus remains on the input while aria-activedescendant tracks the highlighted option. Disabled options are automatically skipped during keyboard navigation.
    <script setup lang="ts">
    import { ref } from "vue"
    import Select from "vue3-select-component"
    import "vue3-select-component/styles"
    
    const selected = ref<string | null>(null)
    </script>
    
    <template>
      <Select
        v-model="selected"
        :options="[
          { label: 'Available', value: '1' },
          { label: 'Unavailable', value: '2', disabled: true },
        ]"
        placeholder="Pick an option"
      />
    </template>
  7. Use extended properties for custom option rendering

    Options can include additional properties beyond label and value. These properties can be accessed when using the primitive components (like SelectOption inside SelectRoot) to create custom-rendered dropdown items.
    <script setup lang="ts">
    import { SelectRoot, SelectOption, ... } from "vue3-select-component/primitives"
    
    const options = [
      { label: "JavaScript", value: "js", version: "ES2022", creator: "Brendan Eich" },
    ]
    </script>
    
    <template>
      <SelectRoot v-model="selected" :options="options">
        <!-- ... other primitives ... -->
        <SelectListbox>
          <SelectOption
            v-for="option in options"
            :key="option.value"
            :value="option.value"
            :label="option.label"
          >
            <span>{{ option.label }} ({{ option.version }})</span>
            <span>{{ option.creator }}</span>
          </SelectOption>
        </SelectListbox>
      </SelectRoot>
    </template>
  8. Implement infinite scroll for remote options

    Infinite scrolling allows loading options in pages as the user scrolls the menu. This is achieved by combining SelectRoot with a custom list component that utilizes @vueuse/core's useInfiniteScroll.

    Implementation strategy:

    1. State Management: The parent component manages the options array, isLoading state, and the totalCount of available remote items.
    2. Initial Load: Use the @menu-opened event on SelectRoot to trigger the first page fetch if the options list is empty.
    3. Appending Data: When the scroll boundary is hit, fetch the next page and append the new options to the existing :options array passed to SelectRoot.
    4. UI Feedback: Use the :loading prop on SelectRoot to show a loading indicator during the initial fetch.
    pnpm add vue3-select-component @vueuse/core
    <!-- InfiniteScrollSelect.vue -->
    <template>
      <SelectRoot
        v-model="selected"
        :options="options"
        :loading="isLoading"
        @menu-opened="onMenuOpened"
      >
        <SelectTrigger>
          <SelectValue placeholder="Pick a post" />
          <SelectTrailingIcon />
        </SelectTrigger>
    
        <SelectPopover>
          <SelectListbox class="infinite-scroll-listbox">
            <InfiniteScrollOptionList
              :loaded-count="options.length"
              :total-count="totalCount"
              :is-loading-more="isLoadingMore"
              :has-more="hasMore"
              @load-more="loadPosts(true)"
            />
          </SelectListbox>
        </SelectPopover>
      </SelectRoot>
    </template>
    
    <style scoped>
    :deep([data-select-listbox].infinite-scroll-listbox) {
      overflow: hidden;
    }
    </style>
  9. Create new items using the create-item feature

    When the create-item prop is enabled, the @create event is emitted when a user chooses to create a new item from the search query. The payload is the trimmed search query as a string. The component does not automatically update options or v-model; the developer must handle the event to update the data source.
    <script setup lang="ts">
    import { ref } from "vue"
    import Select from "vue3-select-component"
    import "vue3-select-component/styles"
    
    const items = ref(["Backlog", "Todo", "Done"])
    const value = ref("Backlog")
    
    function onCreate(item: string) {
      items.value.push(item)
      value.value = item
    }
    </script>
    
    <template>
      <Select
        v-model="value"
        create-item
        :options="items"
        :get-option-label="(item) => item"
        :get-option-value="(item) => item"
        placeholder="Pick or create a status"
        @create="onCreate"
      />
    </template>
  10. Implement custom search filtering with filterBy

    To filter options based on fields other than the label, pass a custom function to the filter-by prop of SelectRoot. This function receives the current option and the search string, returning a boolean to determine if the option should be visible. This is useful for searching across metadata (e.g., searching by both country name and region).
    <script setup lang="ts">
    import { ref } from "vue"
    import { SelectRoot, SelectTrigger, SelectValue, SelectInput, SelectTrailingIcon, SelectPopover, SelectListbox, SelectNoOptions, SelectOption } from "vue3-select-component/primitives"
    import "vue3-select-component/styles"
    
    type CountryRecord = { name: string; code: string; region: string }
    const selected = ref<string | null>(null)
    const countries: CountryRecord[] = [
      { name: "France", code: "FR", region: "Europe" },
      { name: "Japan", code: "JP", region: "Asia" },
      { name: "Brazil", code: "BR", region: "South America" },
      { name: "Canada", code: "CA", region: "North America" },
    ]
    
    function filterByRegion(option: { label: string; value: string }, search: string) {
      const query = search.trim().toLowerCase()
      if (!query) return true
      const record = countries.find((entry) => entry.code === option.value)
      return option.label.toLowerCase().includes(query) || record?.region.toLowerCase().includes(query) === true
    }
    </script>
    
    <template>
      <SelectRoot v-model="selected" searchable :filter-by="filterByRegion">
        <SelectTrigger>
          <SelectValue placeholder="Search by country or region" />
          <SelectInput />
          <SelectTrailingIcon />
        </SelectTrigger>
        <SelectPopover>
          <SelectListbox>
            <SelectNoOptions />
            <SelectOption v-for="country in countries" :key="country.code" :value="country.code" :label="country.name">
              <span>{{ country.name }}</span>
              <span>{{ country.region }}</span>
            </SelectOption>
          </SelectListbox>
        </SelectPopover>
      </SelectRoot>
    </template>
  11. Choose between assembled Select and Primitives

    Decide between the high-level Select component and the low-level Primitives based on your requirements:
    Use assembled SelectUse primitives
    Standard dropdown UXCustom layouts or infinite scroll
    Quick integrationNon-standard trigger or popover behavior
    Migrating from v0Full control over every DOM node