Skip to content

Select ​

A single-choice dropdown, the user picks one option from a list. It supports optional search for long lists, binds the chosen value with v-model, and is built on Headless UI's listbox so keyboard navigation and accessibility come for free.

Reach for Select when the user picks one value from a defined set. Use MultiSelect when they can choose several, and Dropdown for a menu of actions rather than choosing a value.

Anatomy ​

Select                 ← root: holds v-model, provides context, sets placement
├── SelectTrigger      ← the field-like button showing the current selection
└── SelectContent      ← the floating panel (matches the trigger's width)
    ├── SelectSearch   ← optional search input (needs `searchable` on the root)
    └── SelectItem     ← one per option; its content is the label

Each option is a SelectItem with a value and a label. The label is the display text the trigger shows once that option is selected; you can also put richer content inside the item for the dropdown row.

Usage ​

Give each SelectItem a value and a label.

BasicPick one option from a list.
<script setup lang="ts">
import { ref } from "vue";
import { Select, SelectTrigger, SelectContent, SelectItem } from "@dlbcodes/ui";

// Nothing selected yet — the trigger shows its placeholder.
const value = ref("");
</script>

<template>
    <div class="w-full max-w-xs">
        <Select v-model="value">
            <SelectTrigger placeholder="Pick a framework" />
            <SelectContent>
                <SelectItem value="vue" label="Vue">Vue</SelectItem>
                <SelectItem value="react" label="React">React</SelectItem>
                <SelectItem value="svelte" label="Svelte">Svelte</SelectItem>
                <SelectItem value="angular" label="Angular">Angular</SelectItem>
            </SelectContent>
        </Select>
    </div>
</template>

Pre-filled value ​

Because each item carries its label as a prop, a select that's already bound to a value displays the correct label immediately, before the user ever opens it.

Pre-filledA select bound to a value on load shows its label right away.
<script setup lang="ts">
import { ref } from "vue";
import { Select, SelectTrigger, SelectContent, SelectItem } from "@dlbcodes/ui";

// Bound to a value before the user opens the select. Because each item carries
// its `label`, the trigger resolves "React" on load — no opening required.
const value = ref("react");
</script>

<template>
    <div class="w-full max-w-xs">
        <Select v-model="value">
            <SelectTrigger placeholder="Pick a framework" />
            <SelectContent>
                <SelectItem value="vue" label="Vue">Vue</SelectItem>
                <SelectItem value="react" label="React">React</SelectItem>
                <SelectItem value="svelte" label="Svelte">Svelte</SelectItem>
                <SelectItem value="angular" label="Angular">Angular</SelectItem>
            </SelectContent>
        </Select>
    </div>
</template>

Searchable ​

For long lists, add searchable to the root and include a SelectSearch. Items filter themselves by their label as the user types.

SearchableFilter options as you type.
<script setup lang="ts">
import { ref } from "vue";
import { Select, SelectTrigger, SelectContent, SelectItem } from "@dlbcodes/ui";

const value = ref("");
</script>

<template>
    <div class="w-full max-w-xs">
        <Select v-model="value" searchable>
            <SelectTrigger placeholder="Pick a country" />
            <SelectContent>
                <SelectSearch placeholder="Search countries…" />
                <SelectItem value="pt">Portugal</SelectItem>
                <SelectItem value="es">Spain</SelectItem>
                <SelectItem value="fr">France</SelectItem>
                <SelectItem value="de">Germany</SelectItem>
                <SelectItem value="it">Italy</SelectItem>
                <SelectItem value="jp">Japan</SelectItem>
                <SelectItem value="br">Brazil</SelectItem>
                <SelectItem value="ca">Canada</SelectItem>
                <SelectItem value="cn">China</SelectItem>
                <SelectItem value="us">United States</SelectItem>
            </SelectContent>
        </Select>
    </div>
</template>

Rich items ​

The item's slot can hold more than text, an icon, a color dot, a formatted row. The label prop stays the source of truth for the trigger's display text, while the slot controls how the option looks in the list.

Rich contentItems with icons or custom markup.
<script setup lang="ts">
import { ref } from "vue";
import { Select, SelectTrigger, SelectContent, SelectItem } from "@dlbcodes/ui";
import { PhCircle } from "@phosphor-icons/vue";

const value = ref("");
</script>

<template>
    <div class="w-full max-w-xs">
        <Select v-model="value">
            <SelectTrigger placeholder="Set status" />
            <SelectContent>
                <SelectItem value="active">
                    <span class="flex items-center gap-2">
                        <PhCircle
                            class="size-3 text-success-text"
                            weight="fill"
                        />
                        Active
                    </span>
                </SelectItem>
                <SelectItem value="away">
                    <span class="flex items-center gap-2">
                        <PhCircle
                            class="size-3 text-warning-text"
                            weight="fill"
                        />
                        Away
                    </span>
                </SelectItem>
                <SelectItem value="offline">
                    <span class="flex items-center gap-2">
                        <PhCircle
                            class="size-3 text-text-tertiary"
                            weight="fill"
                        />
                        Offline
                    </span>
                </SelectItem>
            </SelectContent>
        </Select>
    </div>
</template>

Disabled options ​

Mark an item disabled to make it non-selectable, it's skipped by keyboard navigation and can't be chosen.

Disabled optionA non-selectable item.
<script setup lang="ts">
import { ref } from "vue";
import { Select, SelectTrigger, SelectContent, SelectItem } from "@dlbcodes/ui";

const value = ref("");
</script>

<template>
    <div class="w-full max-w-xs">
        <Select v-model="value">
            <SelectTrigger placeholder="Choose a plan" />
            <SelectContent>
                <SelectItem value="free">Free</SelectItem>
                <SelectItem value="pro">Pro</SelectItem>
                <SelectItem value="enterprise" disabled>
                    Enterprise (contact sales)
                </SelectItem>
            </SelectContent>
        </Select>
    </div>
</template>

Inside a Field ​

Wrap it in a Field for a label and description with the accessibility wiring, just like the other form controls.

With FieldLabelled select with a description.
<script setup lang="ts">
import { ref } from "vue";
import {
    Select,
    SelectTrigger,
    SelectContent,
    SelectItem,
    Field,
    FieldLabel,
    FieldContent,
    FieldDescription,
} from "@dlbcodes/ui";

const role = ref("");
</script>

<template>
    <Field class="w-full max-w-xs">
        <FieldLabel>Role</FieldLabel>
        <FieldContent>
            <Select v-model="role">
                <SelectTrigger placeholder="Assign a role" />
                <SelectContent>
                    <SelectItem value="viewer">Viewer</SelectItem>
                    <SelectItem value="editor">Editor</SelectItem>
                    <SelectItem value="admin">Admin</SelectItem>
                </SelectContent>
            </Select>
            <FieldDescription
                >Controls what this member can do.</FieldDescription
            >
        </FieldContent>
    </Field>
</template>

Props ​

Select ​

PropTypeDefaultDescription
modelValuestringnoneThe selected value. Use with v-model. Required.
searchablebooleanfalseEnables filtering (include a SelectSearch).
placement"bottom-start" | "bottom-end" | "bottom" | "top-start" | "top-end" | "top""bottom-start"Where the panel opens relative to the trigger.
offsetnumber4Gap (px) between trigger and panel.
classstringnoneClasses merged onto the root.

SelectTrigger ​

PropTypeDefaultDescription
placeholderstring"Select an option"Shown when nothing is selected.
variant"primary" | "contrast""primary"Field style (shares Input's variants).
size"sm" | "base" | "lg" | "xl""base"Field size (shares Input's sizes). Heights are 32, 40, 48 and 56px, the same as Button and Input.
classstringnoneClasses merged onto the trigger.

SelectTrigger exposes selected (the value) and label (the display text) via its default slot, so you can render the selection however you like.

SelectContent ​

PropTypeDefaultDescription
width"fit" | "full" | "3xs" | "2xs" | "xs" | "sm" | "md" | "lg""full"Panel width. full matches the trigger; a token gives a fixed width.
classstringnoneClasses merged onto the panel.

SelectSearch ​

PropTypeDefaultDescription
placeholderstring"Search..."Search input placeholder. Requires searchable.

SelectItem ​

PropTypeDefaultDescription
valuestringnoneThe option's value (what v-model receives). Required.
labelstringnoneDisplay text shown in the trigger when selected. Falls back to the slot's text if omitted.
disabledbooleanfalseMakes the option non-selectable and skipped by keyboard nav.

Provide label so the trigger displays correctly, including for a pre-filled value. The item's slot controls how the option appears in the list (and is used as the label if label is omitted). The slot exposes active and selected for custom rendering.

Accessibility ​

  • Built on Headless UI's Listbox, so arrow-key navigation, type-ahead, Enter to select, Escape to close, and role="listbox" / option wiring are handled.
  • The selected option shows a check and is marked selected for assistive tech.
  • Disabled options are skipped by keyboard navigation.
  • Wrap in a Field (or pair with a Label) so the control has an accessible name.