Fiber UI LogoFiberUI

Select

Displays a list of options for the user to pick from, triggered by a button.

The Select component provides a dropdown list of options, built on top of React Aria's Select component with full keyboard navigation, accessibility, and animation support.

Basic Usage

A simple select with a list of options. The placeholder prop sets the initial text displayed before a selection is made.

import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
} from "@repo/ui/components/select";

/* BASIC USAGE EXAMPLE */
export const Example1 = () => {
    return (
        <Select placeholder="Select a fruit">
            <SelectTrigger className="w-[180px]">
                <SelectValue />
            </SelectTrigger>
            <SelectContent>
                <SelectItem value="apple">Apple</SelectItem>
                <SelectItem value="banana">Banana</SelectItem>
                <SelectItem value="orange">Orange</SelectItem>
                <SelectItem value="grape">Grape</SelectItem>
                <SelectItem value="mango">Mango</SelectItem>
            </SelectContent>
        </Select>
    );
};

Controlled Select

Use the selectedKey and onSelectionChange props to control the select's value programmatically.

Selected: dark

"use client";

import { useState } from "react";
import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
} from "@repo/ui/components/select";

/* CONTROLLED SELECT EXAMPLE */
export const Example2 = () => {
    const [value, setValue] = useState<string>("dark");

    return (
        <div className="space-y-4">
            <Select
                placeholder="Select a theme"
                selectedKey={value}
                onSelectionChange={(key) => setValue(key as string)}
            >
                <SelectTrigger className="w-[180px]">
                    <SelectValue />
                </SelectTrigger>
                <SelectContent>
                    <SelectItem value="light">Light</SelectItem>
                    <SelectItem value="dark">Dark</SelectItem>
                    <SelectItem value="system">System</SelectItem>
                </SelectContent>
            </Select>
            <p className="text-muted-foreground text-sm">
                Selected:{" "}
                <span className="text-foreground font-medium">{value}</span>
            </p>
        </div>
    );
};

Grouped Options

Use SelectGroup and SelectLabel to organize options into logical categories.

import {
    Select,
    SelectContent,
    SelectGroup,
    SelectItem,
    SelectLabel,
    SelectTrigger,
    SelectValue,
} from "@repo/ui/components/select";

/* GROUPED OPTIONS EXAMPLE */
export const Example3 = () => {
    return (
        <Select placeholder="Select a food">
            <SelectTrigger className="w-[200px]">
                <SelectValue />
            </SelectTrigger>
            <SelectContent>
                <SelectGroup>
                    <SelectLabel>Fruits</SelectLabel>
                    <SelectItem value="apple">Apple</SelectItem>
                    <SelectItem value="banana">Banana</SelectItem>
                    <SelectItem value="orange">Orange</SelectItem>
                </SelectGroup>
                <SelectGroup>
                    <SelectLabel>Vegetables</SelectLabel>
                    <SelectItem value="carrot">Carrot</SelectItem>
                    <SelectItem value="broccoli">Broccoli</SelectItem>
                    <SelectItem value="spinach">Spinach</SelectItem>
                </SelectGroup>
            </SelectContent>
        </Select>
    );
};

Disabled State

Disable the entire select with isDisabled, or disable individual items with the isDisabled prop on SelectItem.

import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
} from "@repo/ui/components/select";

/* DISABLED SELECT AND ITEMS EXAMPLE */
export const Example4 = () => {
    return (
        <div className="flex flex-wrap items-start gap-4">
            {/* Disabled Select */}
            <Select placeholder="Disabled select" isDisabled>
                <SelectTrigger className="w-[180px]">
                    <SelectValue />
                </SelectTrigger>
                <SelectContent>
                    <SelectItem value="option1">Option 1</SelectItem>
                    <SelectItem value="option2">Option 2</SelectItem>
                </SelectContent>
            </Select>

            {/* Select with Disabled Items */}
            <Select placeholder="Select an option">
                <SelectTrigger className="w-[180px]">
                    <SelectValue />
                </SelectTrigger>
                <SelectContent>
                    <SelectItem value="available">Available</SelectItem>
                    <SelectItem value="disabled" isDisabled>
                        Disabled Item
                    </SelectItem>
                    <SelectItem value="premium" isDisabled>
                        Premium (Locked)
                    </SelectItem>
                    <SelectItem value="free">Free Option</SelectItem>
                </SelectContent>
            </Select>
        </div>
    );
};

Trigger Sizes

The SelectTrigger supports a size prop with "default" and "sm" options.

import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
} from "@repo/ui/components/select";

/* SMALL TRIGGER SIZE EXAMPLE */
export const Example5 = () => {
    return (
        <div className="flex flex-wrap items-center gap-4">
            <Select placeholder="Default size">
                <SelectTrigger className="w-[150px]">
                    <SelectValue />
                </SelectTrigger>
                <SelectContent>
                    <SelectItem value="xs">Extra Small</SelectItem>
                    <SelectItem value="sm">Small</SelectItem>
                    <SelectItem value="md">Medium</SelectItem>
                    <SelectItem value="lg">Large</SelectItem>
                </SelectContent>
            </Select>

            <Select placeholder="Small size">
                <SelectTrigger className="w-[150px]" size="sm">
                    <SelectValue />
                </SelectTrigger>
                <SelectContent>
                    <SelectItem value="xs">Extra Small</SelectItem>
                    <SelectItem value="sm">Small</SelectItem>
                    <SelectItem value="md">Medium</SelectItem>
                    <SelectItem value="lg">Large</SelectItem>
                </SelectContent>
            </Select>
        </div>
    );
};

Scrollable Content

When you have many options, the dropdown automatically becomes scrollable with a maximum height.

import {
    Select,
    SelectContent,
    SelectGroup,
    SelectItem,
    SelectLabel,
    SelectTrigger,
    SelectValue,
} from "@repo/ui/components/select";

/* SCROLLABLE SELECT WITH MANY OPTIONS */
export const Example6 = () => {
    const timezones = [
        {
            label: "North America",
            items: [
                { value: "est", label: "Eastern Standard Time (EST)" },
                { value: "cst", label: "Central Standard Time (CST)" },
                { value: "mst", label: "Mountain Standard Time (MST)" },
                { value: "pst", label: "Pacific Standard Time (PST)" },
                { value: "akst", label: "Alaska Standard Time (AKST)" },
                { value: "hst", label: "Hawaii Standard Time (HST)" },
            ],
        },
        {
            label: "Europe",
            items: [
                { value: "gmt", label: "Greenwich Mean Time (GMT)" },
                { value: "cet", label: "Central European Time (CET)" },
                { value: "eet", label: "Eastern European Time (EET)" },
                { value: "west", label: "Western European Summer Time (WEST)" },
            ],
        },
        {
            label: "Asia",
            items: [
                { value: "msk", label: "Moscow Time (MSK)" },
                { value: "ist", label: "India Standard Time (IST)" },
                { value: "cst_asia", label: "China Standard Time (CST)" },
                { value: "jst", label: "Japan Standard Time (JST)" },
                { value: "kst", label: "Korea Standard Time (KST)" },
            ],
        },
        {
            label: "Australia & Pacific",
            items: [
                {
                    value: "awst",
                    label: "Australian Western Standard Time (AWST)",
                },
                {
                    value: "acst",
                    label: "Australian Central Standard Time (ACST)",
                },
                {
                    value: "aest",
                    label: "Australian Eastern Standard Time (AEST)",
                },
                { value: "nzst", label: "New Zealand Standard Time (NZST)" },
            ],
        },
    ];

    return (
        <Select placeholder="Select a timezone">
            <SelectTrigger className="w-[280px]">
                <SelectValue />
            </SelectTrigger>
            <SelectContent>
                {timezones.map((group) => (
                    <SelectGroup key={group.label}>
                        <SelectLabel>{group.label}</SelectLabel>
                        {group.items.map((item) => (
                            <SelectItem key={item.value} value={item.value}>
                                {item.label}
                            </SelectItem>
                        ))}
                    </SelectGroup>
                ))}
            </SelectContent>
        </Select>
    );
};

Form Layout

Select components work well with Label components in form layouts.

"use client";

import { useState } from "react";
import { Label } from "@repo/ui/components/label";
import {
    Select,
    SelectContent,
    SelectItem,
    SelectTrigger,
    SelectValue,
} from "@repo/ui/components/select";

/* SELECT IN A FORM LAYOUT */
export const Example7 = () => {
    const [country, setCountry] = useState<string>("");
    const [role, setRole] = useState<string>("");

    return (
        <div className="w-full max-w-sm space-y-4">
            <div className="space-y-2">
                <Label htmlFor="country">Country</Label>
                <Select
                    id="country"
                    placeholder="Select your country"
                    selectedKey={country}
                    onSelectionChange={(key) => setCountry(key as string)}
                >
                    <SelectTrigger className="w-full">
                        <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                        <SelectItem value="us">United States</SelectItem>
                        <SelectItem value="uk">United Kingdom</SelectItem>
                        <SelectItem value="ca">Canada</SelectItem>
                        <SelectItem value="au">Australia</SelectItem>
                        <SelectItem value="de">Germany</SelectItem>
                        <SelectItem value="fr">France</SelectItem>
                        <SelectItem value="in">India</SelectItem>
                    </SelectContent>
                </Select>
            </div>

            <div className="space-y-2">
                <Label htmlFor="role">Role</Label>
                <Select
                    id="role"
                    placeholder="Select your role"
                    selectedKey={role}
                    onSelectionChange={(key) => setRole(key as string)}
                >
                    <SelectTrigger className="w-full">
                        <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                        <SelectItem value="developer">Developer</SelectItem>
                        <SelectItem value="designer">Designer</SelectItem>
                        <SelectItem value="manager">Manager</SelectItem>
                        <SelectItem value="analyst">Analyst</SelectItem>
                    </SelectContent>
                </Select>
            </div>
        </div>
    );
};

Component Code

"use client";

import * as React from "react";
import { CheckIcon, ChevronDownIcon } from "lucide-react";
import {
    Select as AriaSelect,
    SelectValue as AriaSelectValue,
    Button as AriaButton,
    Popover as AriaPopover,
    ListBox as AriaListBox,
    ListBoxItem as AriaListBoxItem,
    ListBoxSection as AriaListBoxSection,
    Header as AriaHeader,
    composeRenderProps,
    type SelectProps as AriaSelectProps,
    type ButtonProps as AriaButtonProps,
    type PopoverProps as AriaPopoverProps,
    type ListBoxProps as AriaListBoxProps,
    type ListBoxItemProps as AriaListBoxItemProps,
    type SectionProps as AriaSectionProps,
} from "react-aria-components";
import { cn } from "tailwind-variants";

/* -----------------------------------------------------------------------------
 * Select (Root)
 * ---------------------------------------------------------------------------*/

interface SelectProps<T extends object> extends AriaSelectProps<T> {}

export const Select = <T extends object>(props: SelectProps<T>) => {
    return <AriaSelect data-slot="select" {...props} />;
};

/* -----------------------------------------------------------------------------
 * SelectTrigger
 * ---------------------------------------------------------------------------*/

interface SelectTriggerProps extends AriaButtonProps {
    size?: "sm" | "default";
}

export const SelectTrigger = ({
    className,
    size = "default",
    children,
    ...props
}: SelectTriggerProps) => {
    return (
        <AriaButton
            data-slot="select-trigger"
            data-size={size}
            className={cn(
                "border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground",
                "focus-visible:border-ring focus-visible:ring-ring/50",
                "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
                "dark:bg-input/30 dark:hover:bg-input/50",
                "shadow-xs flex items-center justify-between gap-2 whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm",
                "outline-none transition-[color,box-shadow] focus-visible:ring-[3px]",
                "disabled:cursor-not-allowed disabled:opacity-50",
                "data-[size=default]:h-9 data-[size=sm]:h-8",
                "*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
                "[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
                className,
            )}
            {...props}
        >
            {composeRenderProps(children, (children) => (
                <>
                    {children}
                    <ChevronDownIcon className="size-4 opacity-50" />
                </>
            ))}
        </AriaButton>
    );
};

/* -----------------------------------------------------------------------------
 * SelectValue
 * ---------------------------------------------------------------------------*/

interface SelectValueProps<T extends object>
    extends React.ComponentProps<typeof AriaSelectValue<T>> {}

export const SelectValue = <T extends object>({
    className,

    ...props
}: SelectValueProps<T>) => {
    return (
        <AriaSelectValue
            data-slot="select-value"
            className={cn(
                "data-placeholder:text-muted-foreground flex-1 text-left",
                className,
            )}
            {...props}
        />
    );
};

/* -----------------------------------------------------------------------------
 * SelectContent
 * ---------------------------------------------------------------------------*/

interface SelectContentProps<T extends object>
    extends AriaPopoverProps,
        Pick<AriaListBoxProps<T>, "items"> {
    children: React.ReactNode;
}

export const SelectContent = <T extends object>({
    className,
    children,
    ...props
}: SelectContentProps<T>) => {
    return (
        <AriaPopover
            data-slot="select-content"
            className={cn(
                "bg-popover text-popover-foreground",
                "data-[entering]:animate-in data-[exiting]:animate-out",
                "data-[exiting]:fade-out-0 data-[entering]:fade-in-0",
                "data-[exiting]:zoom-out-95 data-[entering]:zoom-in-95",
                "data-[placement=bottom]:slide-in-from-top-2 data-[placement=left]:slide-in-from-right-2",
                "data-[placement=right]:slide-in-from-left-2 data-[placement=top]:slide-in-from-bottom-2",
                "relative z-50 overflow-hidden rounded-md border shadow-md",
                className,
            )}
            {...props}
        >
            <AriaListBox
                data-slot="select-listbox"
                className="max-h-[300px] overflow-y-auto p-1 outline-none"
            >
                {children}
            </AriaListBox>
        </AriaPopover>
    );
};

/* -----------------------------------------------------------------------------
 * SelectItem
 * ---------------------------------------------------------------------------*/

interface SelectItemProps extends Omit<AriaListBoxItemProps, "value"> {
    value?: string;
}

export const SelectItem = ({
    className,
    children,
    value,
    id,
    ...props
}: SelectItemProps) => {
    return (
        <AriaListBoxItem
            data-slot="select-item"
            className={cn(
                "focus:bg-accent focus:text-accent-foreground",
                "[&_svg:not([class*='text-'])]:text-muted-foreground",
                "outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-2 pr-8 text-sm",
                "data-disabled:pointer-events-none data-[disabled]:opacity-50",
                "[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
                className,
            )}
            {...props}
            id={id == undefined ? value : id}
        >
            {composeRenderProps(children, (children, { isSelected }) => (
                <>
                    <span className="flex flex-1 items-center gap-2 truncate">
                        {children}
                    </span>
                    {isSelected && (
                        <span
                            data-slot="select-item-indicator"
                            className="absolute right-2 flex size-3.5 items-center justify-center"
                        >
                            <CheckIcon className="size-4" />
                        </span>
                    )}
                </>
            ))}
        </AriaListBoxItem>
    );
};

/* -----------------------------------------------------------------------------
 * SelectGroup
 * ---------------------------------------------------------------------------*/

interface SelectGroupProps<T extends object> extends AriaSectionProps<T> {}

export const SelectGroup = <T extends object>({
    className,
    ...props
}: SelectGroupProps<T>) => {
    return (
        <AriaListBoxSection
            data-slot="select-group"
            className={cn("py-1", className)}
            {...props}
        />
    );
};

/* -----------------------------------------------------------------------------
 * SelectLabel
 * ---------------------------------------------------------------------------*/

interface SelectLabelProps extends React.ComponentProps<typeof AriaHeader> {}

export const SelectLabel = ({ className, ...props }: SelectLabelProps) => {
    return (
        <AriaHeader
            data-slot="select-label"
            className={cn(
                "text-muted-foreground px-2 py-1.5 text-xs font-medium",
                className,
            )}
            {...props}
        />
    );
};