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><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 key | data-slot | Element |
|---|---|---|
base | select | root <div> |
trigger | select-trigger | <button> |
value | select-value | <span> inside the trigger |
clearButton | select-clear-button | <span aria-hidden> inside the trigger |
indicator | select-indicator / select-default-indicator | chevron |
popover | select-popover | popover 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 stringExamples
Variants
primary(default) - Standard styling with shadow, suitable for most use casessecondary- Lower emphasis variant without shadow, suitable for use in Surface components
<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
<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
<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
<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
<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
<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
<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
<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
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
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.
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.
<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
<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.
<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
<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.
<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.
<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
<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:
:hoveror[data-hovered="true"]on the trigger - Focus:
:focus-visibleor[data-focus-visible="true"]on the trigger - Disabled:
:disabledor[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
| Prop | Type | Default | Description |
|---|---|---|---|
items | ListBoxEntry[] | - | 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. |
defaultValue | SelectValue | undefined | The initial selection when uncontrolled (no v-model). |
placeholder | string | "Select an item" | Text shown in the trigger while nothing is selected. |
label | string | - | Label text rendered above the trigger. |
description | string | - | Helper text rendered below the trigger. Hidden while invalid. |
errorMessage | string | - | Error text rendered below the trigger while invalid. Defaults to the validation messages. |
id | string | - | Id of the trigger button. |
name | string | - | Name of the hidden native <select>, submitted with forms. |
autocomplete | string | - | autocomplete hint of the hidden native <select>. |
open | boolean | undefined | Whether the popover is open (v-model:open). |
defaultOpen | boolean | false | Whether the popover starts open when uncontrolled. |
clearable | boolean | false | Whether 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. |
fullWidth | boolean | false | Whether the select takes the full width of its container. |
disabled | boolean | false | Whether the select is disabled. |
required | boolean | false | Whether a selection is required before the form can be submitted. |
invalid | boolean | undefined | Controlled invalid state; overrides validation. |
validate | ValidateFn<SelectValue> | - | Custom validation, called with the current selection. Return an error message (or several) when invalid. |
validationBehavior | ValidationBehavior | 'native' (or the enclosing Form's) | native blocks form submission and shows errors on commit; aria shows errors in realtime without blocking. |
loading | boolean | undefined | Async options: shows the #loading slot after the last option and emits load-more at the end of the list (see ListBox loading). |
ui | ComponentSlots<{ 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. |
modelValue | SelectValue | - | - |
Slots
| Slot | Props | Description |
|---|---|---|
label | any | Label content. Replaces label. |
value | SelectValueSlotProps | Trigger value content. Defaults to the selected label(s) or the placeholder. |
indicator | { open: boolean; } | Trigger indicator content. Defaults to a chevron. |
clear | any | Clear button content. Defaults to a close icon. |
description | any | Description content. Replaces description. |
error | ValidationResult | Error content, rendered while invalid. |
item | ListBoxItemSlotProps | Whole content of each option (forwarded to ListBox #item). |
item-leading | ListBoxItemSlotProps | Content before each option's text (ListBox #item-leading). |
item-label | ListBoxItemSlotProps | Each option's label (ListBox #item-label). |
item-description | ListBoxItemSlotProps | Each option's description (ListBox #item-description). |
item-trailing | ListBoxItemSlotProps | Content after each option's text (ListBox #item-trailing). |
item-indicator | ListBoxItemSlotProps | Option checkmark content (ListBox #indicator). |
section-label | { section: ListBoxSection; } | Section heading (ListBox #section-label). |
empty | any | Rendered in the popover when items is empty (ListBox #empty). |
loading | any | Shown after the last option while loading is true (ListBox #loading). |
Emits
| Event | Payload | Description |
|---|---|---|
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-labelledbycombines the label and the current value; description and error are linked witharia-describedby- A hidden native
<select>carriesname, form data and nativerequiredvalidation







