Single-select menu for choosing from a structured list.
npx shadcn@latest add @iconiq/r-select"use client";
import { useEffect, useState } from "react";
import {
CalendarDays,
MessageSquareText,
Palette,
Rocket,
ShieldCheck,
} from "lucide-react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/r-select";
type WorkflowOption = {
value: string;
label?: string;
};
function getOptionIcon(value: string) {
switch (value) {
case "launch":
return <Rocket className="size-4 text-muted-foreground" />;
case "design":
return <Palette className="size-4 text-muted-foreground" />;
case "review":
return <MessageSquareText className="size-4 text-muted-foreground" />;
case "schedule":
return <CalendarDays className="size-4 text-muted-foreground" />;
case "approve":
return <ShieldCheck className="size-4 text-muted-foreground" />;
default:
return null;
}
}
export function SelectDemo() {
const [options, setOptions] = useState<WorkflowOption[]>([]);
const [value, setValue] = useState("");
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
async function loadOptions() {
const response = await fetch("/api/workflows");
const data: WorkflowOption[] = await response.json();
if (!cancelled) {
setOptions(data);
setLoading(false);
}
}
loadOptions();
return () => {
cancelled = true;
};
}, []);
return (
<div className="flex w-full items-center justify-center px-4 py-6">
<div className="flex w-full max-w-72 flex-col gap-2.5">
<Select
onValueChange={(nextValue) => setValue(nextValue ?? "")}
value={value}>
<SelectTrigger
className="w-full"
description="Choose the next workflow step from one compact select."
>
<SelectValue placeholder={loading ? "Loading..." : "Choose workflow"}>
{(currentValue) => {
const option = options.find((entry) => entry.value === currentValue);
if (!option) {
return currentValue ?? null;
}
return (
<>
{getOptionIcon(option.value)}
option.label ?? option.value
</>
);
}}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{loading ? (
<SelectItem disabled label="Loading..." value="__loading" />
) : options.length === 0 ? (
<SelectItem disabled label="No options" value="__empty" />
) : (
options.map((option) => (
<SelectItem
icon={getOptionIcon(option.value)}
label={option.label}
key={option.value}
value={option.value}
/>
))
)}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
);
}valueControlled selected value. Leave unset with defaultValue for uncontrolled usage.
Type string
defaultValueInitial selected value for uncontrolled usage.
Type string
onValueChangeCalled when a SelectItem is chosen. The menu closes immediately afterward.
Type (value: string) => void
openControlled popup state. Pair with onOpenChange when parent state owns the menu.
Type boolean
defaultOpenInitial popup state for uncontrolled usage.
Type boolean·Default false
onOpenChangeCalled whenever the trigger, keyboard, item choice, or outside interaction opens or closes the menu.
Type (open: boolean) => void
childrenUsually a SelectValue. The chevron icon is appended automatically.
Type ReactNode
sizeData attribute hook for compact trigger variants without changing the default Iconiq styling.
Type "sm" | "default"·Default "default"
labelOptional field label rendered above the trigger and linked with htmlFor.
Type ReactNode
descriptionOptional helper copy rendered above the trigger. Linked to the trigger with aria-describedby.
Type ReactNode
classNameMerged onto the trigger button. Use it for local width such as w-full max-w-48.
Type string
placeholderShown in the trigger when no item is selected.
Type ReactNode
childrenOptional custom trigger content. Use the render prop to resolve labels or icons from your options array when data is loaded dynamically.
Type ReactNode | (value: string | undefined) => ReactNode
classNameMerged onto the value span. The default keeps text truncated inside the trigger.
Type string
sidePreferred side for the menu before collision handling.
Type "top" | "right" | "bottom" | "left"·Default "bottom"
alignHorizontal alignment against the trigger or anchor.
Type "start" | "center" | "end"·Default "start"
sideOffsetGap between trigger and menu. The default matches the prior select spacing.
Type number·Default 8
classNameMerged onto the animated menu panel for local max height, width, or surface overrides.
Type string
valueStable value reported through onValueChange and used to determine the selected checkmark.
Type string
labelOptional display label for the item and selected trigger value. Falls back to textValue, string children, then value.
Type string
childrenOptional custom row content. When omitted, label or value is rendered in the menu.
Type ReactNode
iconOptional leading icon rendered inline with the item label and selected trigger value.
Type ReactNode
textValueOptional plain-text fallback used for trigger display and typeahead when label is omitted.
Type string
disabledPrevents the item from receiving selection.
Type boolean·Default false
classNameMerged onto the row while preserving the animated active highlight and selected checkmark.
Type string
childrenSelectItem rows or nested option content rendered inside the grouped section.
Type ReactNode
classNameMerged with the default grouped section spacing classes.
Type string
childrenSection label text rendered above a SelectGroup.
Type ReactNode
classNameMerged with the uppercase section label typography classes.
Type string
classNameMerged with the default border-token divider classes.
Type string
classNameMerged with the default scroll button layout classes.
Type string
classNameMerged with the default scroll button layout classes.
Type string
Install the exact registry entry shown on the right when you want the component file and its declared runtime dependencies together.
Dependencies: @radix-ui/react-select, motion, lucide-react.
Installs compound Radix select parts with the same exported part names as the Base UI version.
Keeps grouped sections, trigger width matching, keyboard typeahead, and the same trigger and dropdown motion as the prior select component.
The generated registry file is /r/r-select.json.
Contact
Additionally, if you find any bug or issue, feel free to raise an issue.