Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Select

A comprehensive select component with support for single selection, icons, and animations.

<Select>
  <SelectTrigger>
    <SelectValue placeholder="Select an option" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectItem value="banana">Banana</SelectItem>
    <SelectItem value="cherry">Cherry</SelectItem>
    <SelectItem value="date">Date</SelectItem>
    <SelectItem value="elderberry">Elderberry</SelectItem>
  </SelectContent>
</Select>

Installation

Install following dependencies:

npm install @radix-ui/react-select class-variance-authority lucide-react motion
pnpm add @radix-ui/react-select class-variance-authority lucide-react motion
yarn add @radix-ui/react-select class-variance-authority lucide-react motion
bun add @radix-ui/react-select class-variance-authority lucide-react motion

Copy and paste the following code into your project.

components/ui/select.tsx
"use client";

import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { type LucideIcon, ChevronDown, Check } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";

const selectTriggerVariants = cva(
  "flex h-9 w-full items-center justify-between gap-3 rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] px-3 py-2 text-sm transition-all placeholder:text-[hsl(var(--hu-muted-foreground))] focus:outline-none focus:ring-2 focus:ring-[hsl(var(--hu-ring))] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-input))]",
  {
    variants: {
      variant: {
        default:
          "hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))]",
        outline: "border-2 hover:border-[hsl(var(--hu-ring))]",
        ghost:
          "border-transparent hover:bg-[hsl(var(--hu-accent))] hover:text-[hsl(var(--hu-accent-foreground))]",
      },
      size: {
        sm: "h-8 p-2 text-xs gap-2",
        default: "h-9 p-3 gap-3",
        lg: "h-10 p-4 text-base gap-4",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

const selectContentVariants = cva(
  "relative z-50 max-h-[300px] min-w-[8rem] overflow-hidden rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-background))] text-[hsl(var(--hu-foreground))] shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
  {
    variants: {
      position: {
        popper:
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
        "item-aligned": "",
      },
    },
    defaultVariants: {
      position: "popper",
    },
  }
);

const Select = SelectPrimitive.Root;

const SelectGroup = SelectPrimitive.Group;

const SelectValue = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Value>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Value> & {
    placeholder?: string;
  }
>(({ className, placeholder, ...props }, ref) => (
  <SelectPrimitive.Value
    ref={ref}
    className={cn("text-sm select-none", className)}
    placeholder={
      placeholder && (
        <span className="text-[hsl(var(--hu-muted-foreground))] select-none">
          {placeholder}
        </span>
      )
    }
    {...props}
  />
));
SelectValue.displayName = SelectPrimitive.Value.displayName;

interface SelectTriggerProps
  extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>,
    VariantProps<typeof selectTriggerVariants> {
  icon?: LucideIcon;
  placeholder?: string;
}

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  SelectTriggerProps
>(
  (
    { className, children, variant, size, icon: Icon, placeholder, ...props },
    ref
  ) => (
    <SelectPrimitive.Trigger
      ref={ref}
      className={cn(
        "group",
        selectTriggerVariants({ variant, size }),
        className
      )}
      {...props}
    >
      <div className="flex items-center gap-2 flex-1 min-w-0">
        {Icon && (
          <Icon
            size={16}
            className="shrink-0 text-[hsl(var(--hu-muted-foreground))]"
          />
        )}
        <span className="truncate">{children}</span>
      </div>{" "}
      <SelectPrimitive.Icon asChild>
        <ChevronDown
          size={16}
          className="opacity-50 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180"
        />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  )
);
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;

interface SelectContentProps
  extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> {
  position?: "popper" | "item-aligned";
}

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  SelectContentProps
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(selectContentVariants({ position }), className)}
      position={position}
      {...props}
    >
      <motion.div
        initial={{ opacity: 0, scale: 0.95 }}
        animate={{ opacity: 1, scale: 1 }}
        exit={{ opacity: 0, scale: 0.95 }}
        transition={{ duration: 0.15 }}
      >
        <SelectPrimitive.Viewport
          className={cn(
            "p-2 max-h-[280px] overflow-y-auto scrollbar-thin scrollbar-thumb-[hsl(var(--hu-border))] scrollbar-track-transparent",
            position === "popper" &&
              "h-fit w-full min-w-[var(--radix-select-trigger-width)]"
          )}
        >
          {children}
        </SelectPrimitive.Viewport>
      </motion.div>
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;

const SelectLabel = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Label
    ref={ref}
    className={cn(
      "px-3 py-2 text-xs font-semibold text-[hsl(var(--hu-muted-foreground))]",
      className
    )}
    {...props}
  />
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;

interface SelectItemProps
  extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {
  icon?: LucideIcon;
}

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  SelectItemProps
>(({ className, children, icon: Icon, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    className={cn(
      "relative flex w-full cursor-default select-none items-center rounded-lg py-2 pl-3 pr-8 text-sm outline-none focus:bg-[hsl(var(--hu-accent))] focus:text-[hsl(var(--hu-accent-foreground))] data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:text-[hsl(var(--hu-muted-foreground))]",
      className
    )}
    {...props}
  >
    <motion.div
      className="flex w-full items-center gap-2"
      whileHover={{ x: 2 }}
      transition={{ duration: 0.1 }}
    >
      {Icon && <Icon size={16} className="shrink-0" />}
      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
    </motion.div>
    <span className="absolute right-3 flex h-3.5 w-3.5 items-center justify-center">
      <SelectPrimitive.ItemIndicator>
        <motion.div
          initial={{ scale: 0 }}
          animate={{ scale: 1 }}
          transition={{ duration: 0.1 }}
        >
          <Check size={16} />
        </motion.div>
      </SelectPrimitive.ItemIndicator>
    </span>
  </SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-[hsl(var(--hu-muted))]", className)}
    {...props}
  />
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;

export {
  Select,
  SelectGroup,
  SelectValue,
  SelectTrigger,
  SelectContent,
  SelectLabel,
  SelectItem,
  SelectSeparator,
  selectTriggerVariants,
};
npx hextaui@latest add select
pnpm dlx hextaui@latest add select
yarn dlx hextaui@latest add select
bun x hextaui@latest add select

Usage

import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/Select";
<Select>
  <SelectTrigger>
    <SelectValue placeholder="Select an option" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">Option 1</SelectItem>
    <SelectItem value="option2">Option 2</SelectItem>
    <SelectItem value="option3">Option 3</SelectItem>
  </SelectContent>
</Select>

Examples

With Icon

import { User } from "lucide-react";

<Select>
  <SelectTrigger icon={User}>
    <SelectValue placeholder="Select a user" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="john" icon={User}>
      John Doe
    </SelectItem>
    <SelectItem value="jane" icon={User}>
      Jane Smith
    </SelectItem>
    <SelectItem value="bob" icon={User}>
      Bob Johnson
    </SelectItem>
  </SelectContent>
</Select>

With Groups

import { Mail, Phone, MapPin, Home } from "lucide-react";

<Select>
  <SelectTrigger>
    <SelectValue placeholder="Select contact method" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Digital</SelectLabel>
      <SelectItem value="email" icon={Mail}>
        Email
      </SelectItem>
      <SelectItem value="phone" icon={Phone}>
        Phone
      </SelectItem>
    </SelectGroup>
    <SelectSeparator />
    <SelectGroup>
      <SelectLabel>Physical</SelectLabel>
      <SelectItem value="mail" icon={MapPin}>
        Mail
      </SelectItem>
      <SelectItem value="visit" icon={Home}>
        In Person
      </SelectItem>
    </SelectGroup>
  </SelectContent>
</Select>

Variants

<div className="space-y-4">
  {/* Default */}
  <Select>
    <SelectTrigger placeholder="Default variant">
      <SelectValue />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="option1">Option 1</SelectItem>
      <SelectItem value="option2">Option 2</SelectItem>
    </SelectContent>
  </Select>

  {/* Outline */}
  <Select>
    <SelectTrigger variant="outline" placeholder="Outline variant">
      <SelectValue />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="option1">Option 1</SelectItem>
      <SelectItem value="option2">Option 2</SelectItem>
    </SelectContent>
  </Select>

  {/* Ghost */}
  <Select>
    <SelectTrigger variant="ghost" placeholder="Ghost variant">
      <SelectValue />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="option1">Option 1</SelectItem>
      <SelectItem value="option2">Option 2</SelectItem>
    </SelectContent>
  </Select>
</div>

Sizes

<div className="space-y-4">
  {/* Small */}
  <Select>
    <SelectTrigger size="sm">
      <SelectValue placeholder="Small select" />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="option1">Option 1</SelectItem>
      <SelectItem value="option2">Option 2</SelectItem>
    </SelectContent>
  </Select>

  {/* Default */}
  <Select>
    <SelectTrigger>
      <SelectValue placeholder="Default select" />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="option1">Option 1</SelectItem>
      <SelectItem value="option2">Option 2</SelectItem>
    </SelectContent>
  </Select>

  {/* Large */}
  <Select>
    <SelectTrigger size="lg">
      <SelectValue placeholder="Large select" />
    </SelectTrigger>
    <SelectContent>
      <SelectItem value="option1">Option 1</SelectItem>
      <SelectItem value="option2">Option 2</SelectItem>
    </SelectContent>
  </Select>
</div>

Scrollable Content

import { MapPin } from "lucide-react";

const countries = [
  "United States", "Canada", "United Kingdom", "Germany", "France",
  "Italy", "Spain", "Netherlands", "Belgium", "Switzerland",
  "Austria", "Sweden", "Norway", "Denmark", "Finland",
  "Poland", "Czech Republic", "Hungary", "Portugal", "Greece",
  // ... more countries
];

<Select>
  <SelectTrigger icon={MapPin}>
    <SelectValue placeholder="Select a country" />
  </SelectTrigger>
  <SelectContent>
    <SelectGroup>
      <SelectLabel>Countries</SelectLabel>
      {countries.map((country) => (
        <SelectItem key={country} value={country.toLowerCase().replace(/\s+/g, '-')}>
          {country}
        </SelectItem>
      ))}
    </SelectGroup>
  </SelectContent>
</Select>

With Disabled Options

<Select>
  <SelectTrigger>
    <SelectValue placeholder="Select an option" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="available" icon={Star}>
      Available Option
    </SelectItem>
    <SelectItem value="disabled" icon={Heart} disabled>
      Disabled Option
    </SelectItem>
    <SelectItem value="another" icon={Settings}>
      Another Option
    </SelectItem>
  </SelectContent>
</Select>

Disabled Select

<Select disabled>
  <SelectTrigger>
    <SelectValue placeholder="This select is disabled" />
  </SelectTrigger>
  <SelectContent>
    <SelectItem value="option1">Option 1</SelectItem>
    <SelectItem value="option2">Option 2</SelectItem>
  </SelectContent>
</Select>

API Reference

Select

PropTypeDefault
required?
boolean
false
name?
string
-
disabled?
boolean
false
onValueChange?
(value: string) => void
-
defaultValue?
string
-
value?
string
-

SelectTrigger

PropTypeDefault
placeholder?
string
-
icon?
LucideIcon
-
size?
"sm" | "default" | "lg"
"default"
variant?
"default" | "outline" | "ghost"
"default"

SelectItem

PropTypeDefault
icon?
LucideIcon
-
disabled?
boolean
false
value
string
-
Edit on GitHub

Last updated on