Dropdown select with single/multiple value, search, and async data.
Introduction
Select is a custom dropdown built on FormField and Menu—not a native <select> element.
Use Select when you need searchable options, multiple selection, async data, clearable values, or rich option rendering. Use a native <select> when you prefer the platform’s built-in picker (especially on mobile), need maximum simplicity for short static lists, or must submit unmodified HTML form values without JavaScript.
Import
import { Select } from "@bridge-ui/vue/Components/Select";import { Select } from "@bridge-ui/react/Components/Select";Basic usage
Pass an options array of objects with label and value keys (customizable via optionLabel and optionValue). For an initial value without binding, use defaultValue in React or default-value in Vue.
<script setup lang="ts">
import { Select } from "@bridge-ui/vue/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
{ value: "fr", label: "France" },
];
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<Select
label="Country"
:options="countries"
placeholder="Select a country"
/>
</div>
</template>
import { Select } from "@bridge-ui/react/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
{ value: "fr", label: "France" },
];
export default function SelectBasic() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Select
label="Country"
options={countries}
placeholder="Select a country"
/>
</div>
);
}
Sizes
Control the select size with the size prop. Available sizes: 2xs, xs, sm, md (default), lg, xl, and 2xl.
<script setup lang="ts">
import { Select } from "@bridge-ui/vue/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
];
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<Select
size="2xs"
label="2xs"
:options="countries"
placeholder="Select..."
/>
<Select size="xs" label="xs" :options="countries" placeholder="Select..." />
<Select size="sm" label="sm" :options="countries" placeholder="Select..." />
<Select size="md" label="md" :options="countries" placeholder="Select..." />
<Select size="lg" label="lg" :options="countries" placeholder="Select..." />
<Select size="xl" label="xl" :options="countries" placeholder="Select..." />
<Select
size="2xl"
label="2xl"
:options="countries"
placeholder="Select..."
/>
</div>
</template>
import { Select } from "@bridge-ui/react/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
];
export default function SelectSizes() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Select
size="2xs"
label="2xs"
options={countries}
placeholder="Select..."
/>
<Select
size="xs"
label="xs"
options={countries}
placeholder="Select..."
/>
<Select
size="sm"
label="sm"
options={countries}
placeholder="Select..."
/>
<Select
size="md"
label="md"
options={countries}
placeholder="Select..."
/>
<Select
size="lg"
label="lg"
options={countries}
placeholder="Select..."
/>
<Select
size="xl"
label="xl"
options={countries}
placeholder="Select..."
/>
<Select
size="2xl"
label="2xl"
options={countries}
placeholder="Select..."
/>
</div>
);
}
Rounded
Use the rounded prop to control border radius on both the field and the listbox dropdown panel. Available values: none, xs, sm, md (default), lg, xl, 2xl, 3xl, 4xl, and full.
<script setup lang="ts">
import { Select } from "@bridge-ui/vue/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
];
</script>
<template>
<div class="grid w-full max-w-2xl grid-cols-1 gap-4 sm:grid-cols-2">
<Select
label="none"
rounded="none"
:options="countries"
placeholder="Select..."
/>
<Select
label="sm"
rounded="sm"
:options="countries"
placeholder="Select..."
/>
<Select
label="md"
rounded="md"
:options="countries"
placeholder="Select..."
/>
<Select
label="2xl"
rounded="2xl"
:options="countries"
placeholder="Select..."
/>
<Select
label="full"
rounded="full"
:options="countries"
placeholder="Select..."
/>
</div>
</template>
import { Select } from "@bridge-ui/react/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
];
export default function SelectRounded() {
return (
<div className="grid w-full max-w-2xl grid-cols-1 gap-4 sm:grid-cols-2">
<Select
label="none"
rounded="none"
options={countries}
placeholder="Select..."
/>
<Select
label="sm"
rounded="sm"
options={countries}
placeholder="Select..."
/>
<Select
label="md"
rounded="md"
options={countries}
placeholder="Select..."
/>
<Select
label="2xl"
rounded="2xl"
options={countries}
placeholder="Select..."
/>
<Select
label="full"
rounded="full"
options={countries}
placeholder="Select..."
/>
</div>
);
}
Multiple
Enable multiple to allow selecting more than one option. Combine with searchable to filter the list.
<script setup lang="ts">
import { ref } from "vue";
import { Select } from "@bridge-ui/vue/Components/Select";
const frameworks = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
{ label: "Angular", value: "angular" },
];
const selected = ref(["react"]);
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<Select
multiple
searchable
v-model="selected"
label="Frameworks"
:options="frameworks"
/>
</div>
</template>
import { useState } from "react";
import { Select } from "@bridge-ui/react/Components/Select";
const frameworks = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" },
{ label: "Angular", value: "angular" },
];
export default function SelectMultiple() {
const [selected, setSelected] = useState<string[]>(["react"]);
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Select
multiple
searchable
value={selected}
label="Frameworks"
options={frameworks}
onChange={setSelected}
/>
</div>
);
}
Searchable
Set searchable to filter options as the user types. Search UI appears automatically when the option count exceeds minItemsForSearch (default 11).
<script setup lang="ts">
import { Select } from "@bridge-ui/vue/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
{ value: "fr", label: "France" },
{ value: "de", label: "Germany" },
{ value: "it", label: "Italy" },
{ value: "nl", label: "Netherlands" },
{ value: "be", label: "Belgium" },
{ value: "at", label: "Austria" },
{ value: "ch", label: "Switzerland" },
{ value: "gb", label: "United Kingdom" },
];
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<Select
searchable
label="Country"
:options="countries"
placeholder="Type to filter..."
/>
</div>
</template>
import { Select } from "@bridge-ui/react/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
{ value: "fr", label: "France" },
{ value: "de", label: "Germany" },
{ value: "it", label: "Italy" },
{ value: "nl", label: "Netherlands" },
{ value: "be", label: "Belgium" },
{ value: "at", label: "Austria" },
{ value: "ch", label: "Switzerland" },
{ value: "gb", label: "United Kingdom" },
];
export default function SelectSearchable() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Select
searchable
label="Country"
options={countries}
placeholder="Type to filter..."
/>
</div>
);
}
Declarative options
Use SelectOption children (Vue) or the options prop (React) when options are static and defined in markup.
<script setup lang="ts">
import { Select, SelectOption } from "@bridge-ui/vue/Components/Select";
</script>
<template>
<div class="w-full max-w-sm">
<Select label="Priority" placeholder="Choose priority">
<SelectOption label="Low" value="low" />
<SelectOption label="Medium" value="medium" />
<SelectOption label="High" value="high" />
<SelectOption disabled label="Critical" value="critical" />
</Select>
</div>
</template>
import { Select } from "@bridge-ui/react/Components/Select";
const priorityOptions = [
{ label: "Low", value: "low" },
{ label: "Medium", value: "medium" },
{ label: "High", value: "high" },
{ disabled: true, label: "Critical", value: "critical" },
];
const SelectDeclarativeOptions = () => (
<div className="w-full max-w-sm">
<Select
label="Priority"
options={priorityOptions}
placeholder="Choose priority"
/>
</div>
);
export default SelectDeclarativeOptions;
Grouped options
The options prop may mix standalone options and section groups ({ title, options, sticky? }). Search filters within sections and drops empty ones.
<script setup lang="ts">
import { Select } from "@bridge-ui/vue/Components/Select";
const produceOptions = [
{
sticky: true,
title: "Fruits",
options: [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
],
},
{
title: "Vegetables",
options: [
{ label: "Carrot", value: "carrot" },
{ label: "Broccoli", value: "broccoli" },
],
},
{ label: "Other", value: "other" },
];
</script>
<template>
<div class="w-full max-w-sm">
<Select
searchable
label="Produce"
:options="produceOptions"
:min-items-for-search="1"
placeholder="Pick an item"
/>
</div>
</template>
import { Select } from "@bridge-ui/react/Components/Select";
const produceOptions = [
{
sticky: true,
title: "Fruits",
options: [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
],
},
{
title: "Vegetables",
options: [
{ label: "Carrot", value: "carrot" },
{ label: "Broccoli", value: "broccoli" },
],
},
{ label: "Other", value: "other" },
];
export default function SelectGroupedOptions() {
return (
<div className="w-full max-w-sm">
<Select
searchable
label="Produce"
minItemsForSearch={1}
options={produceOptions}
placeholder="Pick an item"
/>
</div>
);
}
Composed list children
Pass ListSection / ListItem as children to build the dropdown list manually. Set value on each ListItem so it registers as a selectable option. When composed children are present, mapped options are not rendered in the listbox.
<script setup lang="ts">
import { ref } from "vue";
import { ListItem } from "@bridge-ui/vue/Components/ListItem";
import { ListSection } from "@bridge-ui/vue/Components/ListSection";
import type { SelectModel } from "@bridge-ui/vue/Components/Select";
import { Select } from "@bridge-ui/vue/Components/Select";
const status = ref<null | SelectModel>(null);
</script>
<template>
<div class="w-full max-w-sm">
<Select label="Status" v-model="status" placeholder="Choose status">
<ListSection sticky title="Workflow" />
<ListItem value="open" primary="Open" />
<ListItem value="closed" primary="Closed" />
<ListSection title="Other" />
<ListItem value="archived" primary="Archived" />
</Select>
</div>
</template>
import { useState } from "react";
import { ListItem } from "@bridge-ui/react/Components/ListItem";
import { ListSection } from "@bridge-ui/react/Components/ListSection";
import type { SelectModel } from "@bridge-ui/react/Components/Select";
import { Select } from "@bridge-ui/react/Components/Select";
export default function SelectComposedChildren() {
const [status, setStatus] = useState<null | SelectModel>(null);
return (
<div className="w-full max-w-sm">
<Select
label="Status"
value={status}
onChange={setStatus}
placeholder="Choose status"
>
<ListSection sticky title="Workflow" />
<ListItem value="open" primary="Open" />
<ListItem value="closed" primary="Closed" />
<ListSection title="Other" />
<ListItem value="archived" primary="Archived" />
</Select>
</div>
);
}
Async data
Pass asyncData with search and resolve callbacks for remote or debounced option lists. Implies searchable. While a search or resolve is in flight, the dropdown shows an indeterminate progress bar and loadingMessage (default "Loading..."). You can also drive loading yourself with the loading prop, or replace the message with the loading slot.
For Laravel backends, see Laravel + Inertia.
<script setup lang="ts">
import type { SelectOptionData, SelectValue } from "@bridge-ui/core";
import { Select } from "@bridge-ui/vue/Components/Select";
import { ref } from "vue";
const value = ref<null | SelectValue>(null);
const allCities = Array.from({ length: 15 }, (_, index) => ({
label: `City ${index + 1}`,
value: `city-${index + 1}`,
}));
const asyncSearch = async (query: string): Promise<SelectOptionData[]> => {
await new Promise((resolve) => setTimeout(resolve, 400));
const normalized = query.trim().toLowerCase();
if (!normalized) return allCities.slice(0, 8);
return allCities.filter((city) =>
city.label.toLowerCase().includes(normalized),
);
};
const asyncResolve = async (
values: SelectValue[],
): Promise<SelectOptionData[]> =>
values.map(
(v) =>
allCities.find((c) => c.value === v) ?? { value: v, label: String(v) },
);
</script>
<template>
<div class="w-full max-w-sm">
<Select
v-model="value"
label="City (async)"
placeholder="Type to search cities..."
:async-data="{ search: asyncSearch, resolve: asyncResolve }"
description="Client-side asyncData with search and resolve callbacks."
/>
</div>
</template>
import { useState } from "react";
import type { SelectOptionData, SelectValue } from "@bridge-ui/core";
import { Select } from "@bridge-ui/react/Components/Select";
const allCities = Array.from({ length: 15 }, (_, index) => ({
label: `City ${index + 1}`,
value: `city-${index + 1}`,
}));
const asyncSearch = async (query: string): Promise<SelectOptionData[]> => {
await new Promise((resolve) => setTimeout(resolve, 400));
const normalized = query.trim().toLowerCase();
if (!normalized) return allCities.slice(0, 8);
return allCities.filter((city) =>
city.label.toLowerCase().includes(normalized),
);
};
const asyncResolve = async (
values: SelectValue[],
): Promise<SelectOptionData[]> =>
values.map(
(v) =>
allCities.find((c) => c.value === v) ?? { value: v, label: String(v) },
);
const SelectAsyncData = () => {
const [value, setValue] = useState<null | SelectValue>(null);
return (
<div className="w-full max-w-sm">
<Select
value={value}
onChange={setValue}
label="City (async)"
placeholder="Type to search cities..."
asyncData={{ search: asyncSearch, resolve: asyncResolve }}
description="Client-side asyncData with search and resolve callbacks."
/>
</div>
);
};
export default SelectAsyncData;
Validation
Set error and errorMessage to show invalid styling and an error message below the field.
<script setup lang="ts">
import { Select } from "@bridge-ui/vue/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
];
</script>
<template>
<div class="flex w-full max-w-sm flex-col gap-4">
<Select
error
label="Country"
:options="countries"
placeholder="Select a country"
error-message="Please select a valid country."
/>
</div>
</template>
import { Select } from "@bridge-ui/react/Components/Select";
const countries = [
{ value: "pt", label: "Portugal" },
{ value: "br", label: "Brazil" },
{ value: "es", label: "Spain" },
];
export default function SelectValidation() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Select
error
label="Country"
options={countries}
placeholder="Select a country"
errorMessage="Please select a valid country."
/>
</div>
);
}
Nested customProps
Use customProps.listbox to forward props to the internal Listbox (including nested customProps for Menu, Progress, List, etc.). Owned Select props such as value and options are not overridable through nested customProps.
<Select
options={options}
customProps={{
label: { id: "country-label" },
listbox: {
customProps: {
menu: { rounded: "xl" },
progress: { size: "sm" },
},
},
}}
/>Related components
Accessibility
Select requires an accessible name from its label (linked via controlId) or an explicit aria-label on the trigger.
- Helper and error text are linked through aria-describedby.
- When error is true, the trigger receives aria-invalid="true".
- The dropdown list uses listbox semantics; keyboard navigation follows WAI-ARIA combobox patterns when searchable is enabled.
Anatomy
Select composes FormField, a trigger (with chips in multiple mode), and a Menu-based options panel:
FormField (root)
├── Header — label, optional corner text, required indicator
├── Container — variant shell
│ └── Trigger — input / chips showing the current selection
├── Options panel — selectable items (supports search, loading, and multiple selection)
└── Footer — description or error messageAPI
| Prop | Type | Default | Description |
|---|---|---|---|
| v-model | SelectModel | — | Two-way binding for the selected value (single or multiple). |
| default-value | SelectModel | — | Initial value for uncontrolled usage (without v-model). |
| Prop | Type | Default | Description |
|---|---|---|---|
| value | SelectModel | — | Selected value (single or multiple). Use with onChange for controlled state. |
| onChange | (value: SelectModel) => void | — | Called when the selection changes. |
Select-specific
| Prop | Type | Default | Description |
|---|---|---|---|
| options | ListboxOptionsInput | — | Options to display. May include section groups ({ title, options, sticky? }) mixed with flat options. |
| children | ReactNode | — | Composed dropdown content (ListSection / ListItem with value). Replaces mapped options. |
| placeholder | string | — | Placeholder when no value is selected. |
| multiple | boolean | false | Whether multiple values can be selected. |
| searchable | boolean | false | Whether options can be filtered via the trigger input. |
| clearable | boolean | true | Whether the value can be cleared. |
| asyncData | SelectAsyncData | — | Remote data source. Implies searchable. |
| loading | boolean | — | External or async loading state (OR’d with async in-flight). |
| loadingMessage | string | "Loading..." | Message shown in the dropdown while loading. |
| defaultValue | SelectModel | null | — | Initial value when uncontrolled. |
| optionLabel | string | "label" | Key used to read the label from option objects. |
| optionValue | string | "value" | Key used to read the value from option objects. |
| optionDescription | string | "description" | Key used to read the description from option objects. |
| minItemsForSearch | number | 11 | Minimum option count before search UI is enabled. |
| emptyMessage | string | "No options" | Message when the filtered list is empty. |
| hideEmptyMessage | boolean | false | Hides the empty-state message. |
| flipOptions | boolean | false | Inverts the visual order of options. |
| maxHeight | string | "max-h-60" | Tailwind max-height class for the dropdown options area. |
| disableMaxHeight | boolean | false | When true, the dropdown options list is not height-limited. |
Events
| Event | Payload | Description | | ———– | ––––––––––– | ——————————————————— | | @clear | — | Called when the value is cleared. | | @close | — | Called when the menu closes. | | @open | — | Called when the menu opens. | | @search | query: string | Called when the search query changes. | | @select | option: SelectOption | Called when an option is selected. | | @deselect | option: SelectOption | Called when an option is deselected (multiple mode). | | @change | value: SelectModel | Called when the value changes (alternative to v-model). |
| Prop | Payload | Description | | ———— | ––––––––––– |
| | onClear | — | Called when the value is cleared. | | onClose | — | Called when the menu closes. | | onOpen | — | Called when the menu opens. | | onSearch | query: string | Called when the search query changes. | | onSelect | option: SelectOption | Called when an option is selected. | | onDeselect | option: SelectOption | Called when an option is deselected (multiple mode). | | onChange | value: SelectModel | Called when the value changes (alternative to the onChange callback prop). |
Slots
| Slot | Description |
|---|---|
| chip | Custom chip content in multiple mode ({ option }). |
| option | Custom option item content ({ option, selected }). |
| loading | Custom loading content in the dropdown (progress bar still renders above). |
| empty | Custom empty-state content. |
| beforeOptions | Content above the option list. |
| afterOptions | Content below the option list. |
Inherited from FormField
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Primary label text above the control. |
| description | string | — | Helper text below the control (hidden when invalid). |
| corner | string | — | Secondary label text at the inline end of the header row. |
| required | boolean | false | Shows a red asterisk on the label. |
| disabled | boolean | false | Whether the control is disabled. |
| readonly | boolean | false | Whether the control is read-only. |
| error | boolean | false | Applies invalid styling and hides description. |
| errorMessage | string | — | Error message below the control. |
| showErrorIcon | boolean | true | Shows an error icon when invalid. |
| hideErrorMessage | boolean | false | Does not reserve space for error messages. |
| variant | FormFieldVariant | "outline" | Visual variant of the field shell. |
| color | FormFieldColor | "primary" | Color applied to the field control. |
| size | FormFieldSize | "md" | Typography and control sizing. |
| rounded | FormFieldRounded | "md" | Border radius of the field control and the listbox dropdown panel. |
| start | string | — | Inline-start text inside the field (prefix). |
| end | string | — | Inline-end text inside the field (suffix). |
| startIcon | IconSource | — | Icon at the inline start. |
| endIcon | IconSource | — | Icon at the inline end. |
| errorIcon | IconSource | "alert" | Icon shown when invalid and showErrorIcon is enabled. |
| controlId | string | — | Associates labels and helper text with the control. Auto-generated when omitted. |
| classes | FormFieldClasses | — | Class overrides per part. |
| customProps | SelectCustomProps | — | FormField parts plus nested listbox, chip, and clearIcon. label accepts Label props (without children). |
| slots | FormFieldSlots | — | React slots. Vue: #label, #corner, default, #description, #errorMessage, #start, #end. |