Select

A select displays a collapsible list of options and allows a user to select one or more of them

Usage

<script setup lang="ts">
import { Select } from '@hareui/vue'
</script>
State
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]</script><template>  <Select class="w-[256px]" label="State" placeholder="Select one" :items="states" /></template>

Anatomy

Select renders a Label, a trigger button showing the selected value, a popover holding a ListBox, and an optional Description / FieldError. Options use ListBox's items format, including sections and separators.

<template>
  <Select label="State" :items="items">
    <template #label />                                  <!-- Label content -->
    <template #value="{ selectedItems, isPlaceholder, text }" /> <!-- trigger value -->
    <template #clear />                                  <!-- clear button content (with `clearable`) -->
    <template #indicator="{ open }" />                   <!-- trigger indicator (default: chevron) -->
    <template #description />                            <!-- Description content -->
    <template #error="validation" />                     <!-- FieldError content -->
    <template #item-leading="{ item, selected }" />      <!-- ListBox item slots, forwarded -->
    <template #item-indicator="{ selected }" />          <!-- option checkmark -->
    <template #loading />                                <!-- with `loading`: after the last option -->
  </Select>
</template>
ui keydata-slotElement
baseselectroot <div>
triggerselect-trigger<button>
valueselect-value<span> inside the trigger
clearButtonselect-clear-button<span aria-hidden> inside the trigger
indicatorselect-indicator / select-default-indicatorchevron
popoverselect-popoverpopover holding the ListBox

Typing items

items takes the exported ListBoxItem type (ListBoxEntry when it mixes in sections and separators). Extra fields are kept: extend the type, and the slots receive your type, so no casts are needed.

import type { ListBoxItem } from '@hareui/vue'

interface MyItem extends ListBoxItem {
  icon: string
}

const items: MyItem[] = [/* ... */]
// <template #item-leading="{ item }"> — item.icon is a string

Examples

Variants

  • primary (default) - Standard styling with shadow, suitable for most use cases
  • secondary - Lower emphasis variant without shadow, suitable for use in Surface components
Primary variant
Secondary variant
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const options: ListBoxItem[] = [  { value: 'option1', label: 'Option 1' },  { value: 'option2', label: 'Option 2' },]</script><template>  <div class="flex flex-col gap-4">    <Select class="w-[256px]" label="Primary variant" placeholder="Select one" variant="primary" :items="options" />    <Select class="w-[256px]" label="Secondary variant" placeholder="Select one" variant="secondary" :items="options" />  </div></template>

Full Width

Favorite Animal
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'cat', label: 'Cat' },  { value: 'dog', label: 'Dog' },  { value: 'bird', label: 'Bird' },]</script><template>  <div class="w-[400px] space-y-4">    <Select full-width label="Favorite Animal" placeholder="Select one" :items="animals" />  </div></template>

With Description

StateSelect your state of residence
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]</script><template>  <Select    class="w-[256px]"    description="Select your state of residence"    label="State"    placeholder="Select one"    :items="states"  /></template>

Required

State
Country
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Button, Form, Select } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const countries: ListBoxItem[] = [  { value: 'usa', label: 'United States' },  { value: 'canada', label: 'Canada' },  { value: 'mexico', label: 'Mexico' },  { value: 'uk', label: 'United Kingdom' },  { value: 'france', label: 'France' },  { value: 'germany', label: 'Germany' },]function onSubmit(event: SubmitEvent) {  const data = Object.fromEntries(new FormData(event.currentTarget as HTMLFormElement))  console.log('Form submitted:', data)  alert('Form submitted successfully!')}</script><template>  <Form class="flex w-[256px] flex-col gap-4" @submit.prevent="onSubmit">    <Select required class="w-full" label="State" name="state" placeholder="Select one" :items="states" />    <Select required class="w-full" label="Country" name="country" placeholder="Select a country" :items="countries" />    <Button type="submit">      Submit    </Button>  </Form></template>

Disabled

State
Countries to Visit
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const countries: ListBoxItem[] = [  { value: 'argentina', label: 'Argentina' },  { value: 'venezuela', label: 'Venezuela' },  { value: 'japan', label: 'Japan' },  { value: 'france', label: 'France' },  { value: 'italy', label: 'Italy' },  { value: 'spain', label: 'Spain' },]</script><template>  <div class="flex flex-col gap-4">    <Select disabled class="w-[256px]" default-value="california" label="State" placeholder="Select one" :items="states" />    <Select      disabled      class="w-[256px]"      label="Countries to Visit"      placeholder="Select countries"      selection-mode="multiple"      :default-value="['argentina', 'japan', 'france']"      :items="countries"    />  </div></template>

With Disabled Options

Animal
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const animals: ListBoxItem[] = [  { value: 'dog', label: 'Dog' },  { value: 'cat', label: 'Cat', disabled: true },  { value: 'bird', label: 'Bird' },  { value: 'kangaroo', label: 'Kangaroo', disabled: true },  { value: 'elephant', label: 'Elephant' },  { value: 'tiger', label: 'Tiger' },]</script><template>  <Select class="w-[256px]" label="Animal" placeholder="Select an animal" :items="animals" /></template>

Multiple Select

Countries to Visit
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const countries: ListBoxItem[] = [  { value: 'argentina', label: 'Argentina' },  { value: 'venezuela', label: 'Venezuela' },  { value: 'japan', label: 'Japan' },  { value: 'france', label: 'France' },  { value: 'italy', label: 'Italy' },  { value: 'spain', label: 'Spain' },  { value: 'thailand', label: 'Thailand' },  { value: 'new-zealand', label: 'New Zealand' },  { value: 'iceland', label: 'Iceland' },]</script><template>  <Select    class="w-[256px]"    label="Countries to Visit"    placeholder="Select countries"    selection-mode="multiple"    :items="countries"  /></template>

With Sections

Country
<script setup lang="ts">import type { ListBoxEntry } from '@hareui/vue'import { Select } from '@hareui/vue'const countries: ListBoxEntry[] = [  { type: 'section', label: 'North America', items: [    { value: 'usa', label: 'United States' },    { value: 'canada', label: 'Canada' },    { value: 'mexico', label: 'Mexico' },  ] },  { type: 'separator' },  { type: 'section', label: 'Europe', items: [    { value: 'uk', label: 'United Kingdom' },    { value: 'france', label: 'France' },    { value: 'germany', label: 'Germany' },    { value: 'spain', label: 'Spain' },    { value: 'italy', label: 'Italy' },  ] },  { type: 'separator' },  { type: 'section', label: 'Asia', items: [    { value: 'japan', label: 'Japan' },    { value: 'china', label: 'China' },    { value: 'india', label: 'India' },    { value: 'south-korea', label: 'South Korea' },  ] },]</script><template>  <Select class="w-[256px]" label="Country" placeholder="Select a country" :items="countries" /></template>

Controlled

State (controlled)

Selected: California

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'import { computed, ref } from 'vue'const states: ListBoxItem[] = [  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'florida', label: 'Florida' },  { value: 'new-york', label: 'New York' },  { value: 'illinois', label: 'Illinois' },  { value: 'pennsylvania', label: 'Pennsylvania' },]const state = ref<string | null>('california')const selectedState = computed(() => states.find(s => s.value === state.value))</script><template>  <div class="space-y-2">    <Select v-model="state" class="w-[256px]" label="State (controlled)" placeholder="Select a state" :items="states" />    <p class="text-sm text-muted">      Selected: {{ selectedState?.label || 'None' }}    </p>  </div></template>

Controlled Multiple

States (controlled multiple)

Selected: california, texas

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'import { ref } from 'vue'const states: ListBoxItem[] = [  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'florida', label: 'Florida' },  { value: 'new-york', label: 'New York' },  { value: 'illinois', label: 'Illinois' },  { value: 'pennsylvania', label: 'Pennsylvania' },]const selected = ref<string[]>(['california', 'texas'])</script><template>  <div class="space-y-4">    <Select      v-model="selected"      class="w-[256px]"      label="States (controlled multiple)"      placeholder="Select states"      selection-mode="multiple"      :items="states"    />    <p class="text-sm text-muted">      Selected: {{ selected.length > 0 ? selected.join(', ') : 'None' }}    </p>  </div></template>

Controlled Open State

Bind v-model:open to open and close the popover from outside.

State

Select is closed

<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Button, Select } from '@hareui/vue'import { ref } from 'vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const isOpen = ref(false)</script><template>  <div class="space-y-4">    <Select v-model:open="isOpen" class="w-[256px]" label="State" placeholder="Select one" :items="states" />    <Button @click="isOpen = !isOpen">      {{ isOpen ? 'Close' : 'Open' }} Select    </Button>    <p class="text-sm text-muted">      Select is {{ isOpen ? 'open' : 'closed' }}    </p>  </div></template>

Asynchronous Loading

Bind loading and listen to load-more: it fires whenever the end of the list scrolls into view while loading is false. The #loading slot shows after the last option while loading.

Pick a Pokemon
<script setup lang="ts">import { Select, Spinner } from '@hareui/vue'import { onMounted, ref } from 'vue'interface Pokemon {  name: string}// HeroUI's `useAsyncList` equivalent: fetch the first page, then the next cursor on `load-more`const pokemon = ref<Pokemon[]>([])const loading = ref(false)let cursor: string | null = 'https://pokeapi.co/api/v2/pokemon'async function loadMore() {  if (!cursor || loading.value)    return  loading.value = true  try {    const json = await (await fetch(cursor)).json()    cursor = json.next    pokemon.value = [...pokemon.value, ...json.results]  }  finally {    loading.value = false  }}// Client only: VitePress SSR-renders demosonMounted(loadMore)</script><template>  <Select    class="w-[256px]"    label="Pick a Pokemon"    placeholder="Select a Pokemon"    :items="pokemon.map(p => ({ value: p.name, label: p.name }))"    :loading="loading"    @load-more="loadMore"  >    <template #loading>      <div class="flex items-center justify-center gap-2 py-2">        <Spinner size="sm" />        <span class="text-sm text-muted">Loading more...</span>      </div>    </template>  </Select></template>

Custom Indicator

State
<script setup lang="ts">import { Icon } from '@iconify/vue'import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]</script><template>  <Select class="w-[256px]" label="State" placeholder="Select one" :items="states" :ui="{ indicator: 'size-3' }">    <template #indicator>      <Icon icon="gravity-ui:chevrons-expand-vertical" />    </template>  </Select></template>

With Clear Button

The trigger is a button, so the clear control can't be a nested <button>. With clearable, Select renders a non-button clear control inside the trigger that doesn't open the menu.

State
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]</script><template>  <Select    clearable    class="w-[256px]"    default-value="california"    label="State"    placeholder="Select one"    :items="states"  /></template>

Because ARIA treats the children of a button as presentational, the clear control can't take focus. It is aria-hidden and works with the pointer only. With clearable, the trigger also clears on Backspace or Delete, so keyboard and screen reader users can clear the selection. Both paths emit clear.

Custom Value

User
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Avatar, Select } from '@hareui/vue'interface User extends ListBoxItem {  avatarUrl: string  fallback: string}const users: User[] = [  { value: '1', label: 'Bob', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg', fallback: 'B' },  { value: '2', label: 'Fred', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg', fallback: 'F' },  { value: '3', label: 'Martha', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg', fallback: 'M' },  { value: '4', label: 'John', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg', fallback: 'J' },  { value: '5', label: 'Jane', description: '[email protected]', avatarUrl: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg', fallback: 'J' },]</script><template>  <Select class="w-[256px]" label="User" placeholder="Select a user" :items="users">    <template #value="{ selectedItems, isPlaceholder, text }">      <template v-if="isPlaceholder">        {{ text }}      </template>      <template v-else-if="selectedItems.length > 1">        {{ selectedItems.length }} users selected      </template>      <div v-else class="flex items-center gap-2">        <Avatar class="size-4" size="sm" :src="selectedItems[0]!.avatarUrl" :fallback="selectedItems[0]!.fallback" />        <span>{{ selectedItems[0]!.label }}</span>      </div>    </template>    <template #item-leading="{ item }">      <Avatar size="sm" :src="item.avatarUrl" :fallback="item.fallback" />    </template>  </Select></template>

Render Function

HeroUI's render prop has no HareUI equivalent; attributes such as data-custom fall through to the root.

State
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]</script><template>  <!-- HeroUI's `render` prop has no Vue equivalent (plan drop: render functions); attributes fall through to the root instead. -->  <Select class="w-[256px]" data-custom="foo" label="State" placeholder="Select one" :items="states" /></template>

In Surface

Inside a Surface, use variant="secondary" for the lower-emphasis look that suits surface backgrounds.

State
Country
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Button, Form, Select, Surface } from '@hareui/vue'const states: ListBoxItem[] = [  { value: 'florida', label: 'Florida' },  { value: 'delaware', label: 'Delaware' },  { value: 'california', label: 'California' },  { value: 'texas', label: 'Texas' },  { value: 'new-york', label: 'New York' },  { value: 'washington', label: 'Washington' },]const countries: ListBoxItem[] = [  { value: 'usa', label: 'United States' },  { value: 'canada', label: 'Canada' },  { value: 'mexico', label: 'Mexico' },  { value: 'uk', label: 'United Kingdom' },  { value: 'france', label: 'France' },  { value: 'germany', label: 'Germany' },]function onSubmit(event: SubmitEvent) {  const data = Object.fromEntries(new FormData(event.currentTarget as HTMLFormElement))  console.log('Form submitted:', data)  alert('Form submitted successfully!')}</script><template>  <Surface class="w-[320px] rounded-3xl p-6">    <Form class="flex w-full flex-col gap-4" @submit.prevent="onSubmit">      <Select required class="w-full" label="State" name="state" placeholder="Select one" variant="secondary" :items="states" />      <Select required class="w-full" label="Country" name="country" placeholder="Select a country" variant="secondary" :items="countries" />      <Button type="submit">        Submit      </Button>    </Form>  </Surface></template>

Customization

Tailwind CSS

Plan
<script setup lang="ts">import type { ListBoxItem } from '@hareui/vue'import { Select } from '@hareui/vue'const itemClass = 'rounded-lg data-[focused=true]:bg-accent/10 data-[selected=true]:text-foreground'const plans: ListBoxItem[] = [  { value: 'free', label: 'Free', class: itemClass },  { value: 'pro', label: 'Pro', class: itemClass },]</script><template>  <Select    class="w-56"    label="Plan"    placeholder="Pick a plan"    variant="secondary"    :items="plans"    :ui="{ trigger: 'rounded-xl bg-default', popover: 'rounded-xl border border-border bg-surface p-1 shadow-lg' }"  /></template>

Global Configuration

app.use(createHareUI({
  ui: { select: { slots: { trigger: 'rounded-lg', popover: 'rounded-lg' } } },
}))

Styling Reference

Interactive States

  • Hover: :hover or [data-hovered="true"] on the trigger
  • Focus: :focus-visible or [data-focus-visible="true"] on the trigger
  • Disabled: :disabled or [data-disabled="true"] on the select
  • Invalid: [data-invalid="true"] on the select
  • Open: [data-open="true"] on the select and the indicator

API Reference

Props

PropTypeDefaultDescription
itemsListBoxEntry[]-Options, sections ({ type: 'section', label, items }) and separators ({ type: 'separator' }), as in ListBox.
selectionMode"multiple" | "single""single"Whether one or several options can be selected. In 'multiple' mode v-model is an array.
defaultValueSelectValueundefinedThe initial selection when uncontrolled (no v-model).
placeholderstring"Select an item"Text shown in the trigger while nothing is selected.
labelstring-Label text rendered above the trigger.
descriptionstring-Helper text rendered below the trigger. Hidden while invalid.
errorMessagestring-Error text rendered below the trigger while invalid. Defaults to the validation messages.
idstring-Id of the trigger button.
namestring-Name of the hidden native <select>, submitted with forms.
autocompletestring-autocomplete hint of the hidden native <select>.
openbooleanundefinedWhether the popover is open (v-model:open).
defaultOpenbooleanfalseWhether the popover starts open when uncontrolled.
clearablebooleanfalseWhether to render a clear button in the trigger. Backspace / Delete on the trigger also clears.
variant"primary" | "secondary"'primary'Visual variant. Use secondary on surfaces.
fullWidthbooleanfalseWhether the select takes the full width of its container.
disabledbooleanfalseWhether the select is disabled.
requiredbooleanfalseWhether a selection is required before the form can be submitted.
invalidbooleanundefinedControlled invalid state; overrides validation.
validateValidateFn<SelectValue>-Custom validation, called with the current selection. Return an error message (or several) when invalid.
validationBehaviorValidationBehavior'native' (or the enclosing Form's)native blocks form submission and shows errors on commit; aria shows errors in realtime without blocking.
loadingbooleanundefinedAsync options: shows the #loading slot after the last option and emits load-more at the end of the list (see ListBox loading).
uiComponentSlots<{ slots: { base: string; trigger: string[]; value: string[]; clearButton: string[]; indicator: string[]; popover: string[]; }; variants: { variant: { primary: {}; secondary: { trigger: string; }; }; fullWidth: { true: { base: string; trigger: string; }; false: {}; }; }; defaultVariants: { variant: string; fullWidth: boolean; }; }>-Per-slot class overrides.
modelValueSelectValue--

Slots

SlotPropsDescription
labelanyLabel content. Replaces label.
valueSelectValueSlotPropsTrigger value content. Defaults to the selected label(s) or the placeholder.
indicator{ open: boolean; }Trigger indicator content. Defaults to a chevron.
clearanyClear button content. Defaults to a close icon.
descriptionanyDescription content. Replaces description.
errorValidationResultError content, rendered while invalid.
itemListBoxItemSlotPropsWhole content of each option (forwarded to ListBox #item).
item-leadingListBoxItemSlotPropsContent before each option's text (ListBox #item-leading).
item-labelListBoxItemSlotPropsEach option's label (ListBox #item-label).
item-descriptionListBoxItemSlotPropsEach option's description (ListBox #item-description).
item-trailingListBoxItemSlotPropsContent after each option's text (ListBox #item-trailing).
item-indicatorListBoxItemSlotPropsOption checkmark content (ListBox #indicator).
section-label{ section: ListBoxSection; }Section heading (ListBox #section-label).
emptyanyRendered in the popover when items is empty (ListBox #empty).
loadinganyShown after the last option while loading is true (ListBox #loading).

Emits

EventPayloadDescription
clear[]-
update:open[value: boolean]-
loadMore[]-
update:modelValue[value: SelectValue | undefined]-

Accessibility

Select follows the ARIA listbox pattern:

  • The trigger opens the list with click, Enter, Space or the arrow keys; focus moves into the list
  • Typing on the closed trigger selects the next matching option (typeahead)
  • aria-labelledby combines the label and the current value; description and error are linked with aria-describedby
  • A hidden native <select> carries name, form data and native required validation

On this page

No Headings