Select

Single-select menu for choosing from a structured list.

Installation

npx shadcn@latest add @iconiq/r-select

File Structure

Usage

"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>
  );
}

Props

Props
Description

Select

value

Controlled selected value. Leave unset with defaultValue for uncontrolled usage.

Type string

defaultValue

Initial selected value for uncontrolled usage.

Type string

onValueChange

Called when a SelectItem is chosen. The menu closes immediately afterward.

Type (value: string) => void

open

Controlled popup state. Pair with onOpenChange when parent state owns the menu.

Type boolean

defaultOpen

Initial popup state for uncontrolled usage.

Type boolean·Default false

onOpenChange

Called whenever the trigger, keyboard, item choice, or outside interaction opens or closes the menu.

Type (open: boolean) => void

SelectTrigger

children

Usually a SelectValue. The chevron icon is appended automatically.

Type ReactNode

size

Data attribute hook for compact trigger variants without changing the default Iconiq styling.

Type "sm" | "default"·Default "default"

label

Optional field label rendered above the trigger and linked with htmlFor.

Type ReactNode

description

Optional helper copy rendered above the trigger. Linked to the trigger with aria-describedby.

Type ReactNode

className

Merged onto the trigger button. Use it for local width such as w-full max-w-48.

Type string

SelectValue

placeholder

Shown in the trigger when no item is selected.

Type ReactNode

children

Optional 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

className

Merged onto the value span. The default keeps text truncated inside the trigger.

Type string

SelectContent

side

Preferred side for the menu before collision handling.

Type "top" | "right" | "bottom" | "left"·Default "bottom"

align

Horizontal alignment against the trigger or anchor.

Type "start" | "center" | "end"·Default "start"

sideOffset

Gap between trigger and menu. The default matches the prior select spacing.

Type number·Default 8

className

Merged onto the animated menu panel for local max height, width, or surface overrides.

Type string

SelectItem

value

Stable value reported through onValueChange and used to determine the selected checkmark.

Type string

label

Optional display label for the item and selected trigger value. Falls back to textValue, string children, then value.

Type string

children

Optional custom row content. When omitted, label or value is rendered in the menu.

Type ReactNode

icon

Optional leading icon rendered inline with the item label and selected trigger value.

Type ReactNode

textValue

Optional plain-text fallback used for trigger display and typeahead when label is omitted.

Type string

disabled

Prevents the item from receiving selection.

Type boolean·Default false

className

Merged onto the row while preserving the animated active highlight and selected checkmark.

Type string

SelectGroup

children

SelectItem rows or nested option content rendered inside the grouped section.

Type ReactNode

className

Merged with the default grouped section spacing classes.

Type string

SelectLabel

children

Section label text rendered above a SelectGroup.

Type ReactNode

className

Merged with the uppercase section label typography classes.

Type string

SelectSeparator

className

Merged with the default border-token divider classes.

Type string

SelectScrollUpButton

className

Merged with the default scroll button layout classes.

Type string

SelectScrollDownButton

className

Merged with the default scroll button layout classes.

Type string

Registry bundle

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.

Choose the next workflow step from one compact select.

Pick a workflow step to continue