<Select.Root
width="280px"
size="sm"
variant="outline"
>
<Select.Label>Project status</Select.Label>
<Select.Field value="Active" placeholder="Choose a status">
<Select.Item>Planned</Select.Item>
<Select.Item selected>Active</Select.Item>
<Select.Item>Done</Select.Item>
</Select.Field>
<Select.ErrorMessage>Choose a status to continue.</Select.ErrorMessage>
</Select.Root>Features
- Selects one option or several.
- Searches long lists.
- Loads options from items or an async source.
- Shows helper text, errors, and loading states.
Anatomy
<Select.Root>
<Select.Label>Project status</Select.Label>
<Select.Field placeholder="Choose a status">
<Select.SectionLabel>Open</Select.SectionLabel>
<Select.Item>Active</Select.Item>
<Select.Item>Done</Select.Item>
</Select.Field>
<Select.HelperText>Choose the current stage of the project.</Select.HelperText>
<Select.ErrorMessage>Choose a status.</Select.ErrorMessage>
</Select.Root>Select.Combobox instead to add search.error is set.Loaded Options
Pass options with loadOptions. Keep the selected option ID in state with value and onValueChange. Define loadOptions outside the component or memoize it; a new function reloads the options.
"use client";
import { Select } from "@vortexlabs/vortex";
import type { SelectLoadOptions } from "@vortexlabs/vortex";
import React from "react";
const loadStatuses: SelectLoadOptions = () => [
{ value: "planned", label: "Planned" },
{ value: "active", label: "Active" },
{ value: "done", label: "Done" },
];
export const SelectLoadedExample = () => {
const [value, setValue] = React.useState("planned");
return (
<Select.Root width="100%">
<Select.Label>Project status</Select.Label>
<Select.Field
value={value}
onValueChange={(next) => setValue(next ?? "")}
loadOptions={loadStatuses}
placeholder="Choose a status"
/>
<Select.HelperText>Choose the current stage of the project.</Select.HelperText>
</Select.Root>
);
};Static Items
Use Select.Item children for sections or custom rows. Mark the current item selected, update state in onClick, and pass its text as value.
"use client";
import { Select } from "@vortexlabs/vortex";
import React from "react";
export const SelectStaticExample = () => {
const [value, setValue] = React.useState("Design");
return (
<Select.Root width="100%">
<Select.Label>Team</Select.Label>
<Select.Field value={value} placeholder="Choose a team">
<Select.SectionLabel>Product</Select.SectionLabel>
<Select.Item selected={value === "Design"} onClick={() => setValue("Design")}>
Design
</Select.Item>
<Select.Item
selected={value === "Engineering"}
onClick={() => setValue("Engineering")}
>
Engineering
</Select.Item>
<Select.SectionLabel>Operations</Select.SectionLabel>
<Select.Item selected={value === "Support"} onClick={() => setValue("Support")}>
Support
</Select.Item>
</Select.Field>
</Select.Root>
);
};Search
Select.Combobox adds a search input and passes the query to loadOptions, which can return a promise. Set selection="single" to pick one option.
"use client";
import { Select } from "@vortexlabs/vortex";
import type { SelectLoadOptions } from "@vortexlabs/vortex";
import React from "react";
const loadProjects: SelectLoadOptions = async (query) => {
await new Promise((resolve) => setTimeout(resolve, 600));
return [
{ value: "website", label: "Website redesign" },
{ value: "mobile", label: "Mobile app" },
{ value: "system", label: "Design system" },
].filter((option) => option.label.toLowerCase().includes(query.toLowerCase()));
};
export const SelectAsyncExample = () => {
const [project, setProject] = React.useState<string | null>(null);
return (
<Select.Root width="100%">
<Select.Label>Project</Select.Label>
<Select.Combobox
selection="single"
loadOptions={loadProjects}
selectedValue={project}
onSelectedValueChange={setProject}
placeholder="Search projects"
loadingLabel="Loading projects"
emptyState="No projects match your search"
/>
</Select.Root>
);
};Multiple Selection
Combobox defaults to multiple selection, shown as removable chips. onSelectedValuesChange receives the updated IDs, including after a chip is removed.
design
"use client";
import { Select } from "@vortexlabs/vortex";
import type { SelectLoadOptions } from "@vortexlabs/vortex";
import React from "react";
const loadSkills: SelectLoadOptions = (query) =>
[
{ value: "design", label: "Design" },
{ value: "engineering", label: "Engineering" },
{ value: "research", label: "Research" },
].filter((option) => option.label.toLowerCase().includes(query.toLowerCase()));
export const SelectMultipleExample = () => {
const [skills, setSkills] = React.useState<string[]>(["design"]);
return (
<Select.Root width="100%">
<Select.Label>Skills</Select.Label>
<Select.Combobox
loadOptions={loadSkills}
selectedValues={skills}
onSelectedValuesChange={setSkills}
placeholder="Add skills"
emptyState="No matching skills"
/>
<Select.HelperText>
Choose all the skills needed for this project.
</Select.HelperText>
</Select.Root>
);
};Sizes and Variants
Set size on Select.Root to size the trigger, label, menu items, and Combobox badges together.
Set size on Select.Item to override that item's text and selection indicator size. Combobox badges still follow Select.Root.size.
"use client";
import { Select, Stack } from "@vortexlabs/vortex";
import type { SelectLoadOptions } from "@vortexlabs/vortex";
import React from "react";
const loadPriorities: SelectLoadOptions = () => [
{ value: "normal", label: "Normal" },
{ value: "high", label: "High" },
];
export const SelectAppearanceExample = () => {
const [value, setValue] = React.useState("normal");
return (
<Stack gap={4} width="100%">
<Select.Root size="sm" variant="outline">
<Select.Label>Small outline</Select.Label>
<Select.Field
loadOptions={loadPriorities}
value={value}
onValueChange={(next) => setValue(next ?? "")}
/>
</Select.Root>
<Select.Root size="md" variant="outline">
<Select.Label>Medium outline</Select.Label>
<Select.Field
loadOptions={loadPriorities}
value={value}
onValueChange={(next) => setValue(next ?? "")}
/>
</Select.Root>
<Select.Root size="sm" variant="fill">
<Select.Label>Small fill</Select.Label>
<Select.Field
loadOptions={loadPriorities}
value={value}
onValueChange={(next) => setValue(next ?? "")}
/>
</Select.Root>
</Stack>
);
};Disabled and Error States
Select.ErrorMessage shows only while error is true; this example sets it after the menu closes without a choice. Select.HelperText always shows.
"use client";
import { Select, Stack } from "@vortexlabs/vortex";
import type { SelectLoadOptions } from "@vortexlabs/vortex";
import React from "react";
const loadRegions: SelectLoadOptions = () => [
{ value: "au", label: "Australia" },
{ value: "eu", label: "Europe" },
];
export const SelectStatesExample = () => {
const [region, setRegion] = React.useState("");
const [touched, setTouched] = React.useState(false);
return (
<Stack gap={4} width="100%">
<Select.Root disabled>
<Select.Label>Managed region</Select.Label>
<Select.Field loadOptions={loadRegions} value="au" />
<Select.HelperText>Managed by your workspace.</Select.HelperText>
</Select.Root>
<Select.Root error={touched && !region}>
<Select.Label>Storage region</Select.Label>
<Select.Field
loadOptions={loadRegions}
value={region}
onValueChange={(next) => setRegion(next ?? "")}
onClose={() => setTouched(true)}
placeholder="Choose a region"
/>
<Select.ErrorMessage>Choose a region before continuing.</Select.ErrorMessage>
</Select.Root>
</Stack>
);
};Accessibility
Select.Field, same as Enter. In a combobox, Space types into the search.Select.Label to name the field. Helper and error text are linked to it automatically.