Built on @radix-ui/react-select — 9 parts, but Select/SelectTrigger/SelectValue/SelectContent/SelectItem cover almost every use. Focus ring is unified to primary-30 (the source's trigger uses a separate theme-token ring — a documented inconsistency this bundle fixes).
npx chandra-asri-ui add select<Select>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
<SelectItem value="starter">Starter</SelectItem>
<SelectItem value="growth">Growth</SelectItem>
<SelectItem value="scale">Scale</SelectItem>
</SelectContent>
</Select>The source platform also has a second select pattern, CustomSelect (components/ui/select/custom-select.tsx), used in 9 pricing/backlink files. It takes an options: { label, value }[] prop instead of composing SelectItem children by hand — better for simple, data-driven dropdowns. Its background-when-has-a-value used bg-blue-100, which isn't a real step on the brand primary-* scale (likely a leftover default-Tailwind class) — not reproduced here. Not shipped as a separate component in this bundle; build it as a thin wrapper around the Select above if you need the array-driven API.
"use client";
import { cn } from "@/lib/utils";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import * as React from "react";
/**
* Mirrors the base guideline Select (built on @radix-ui/react-select) with
* a few deliberate fixes: the source's trigger uses a theme-token focus
* ring (`ring-ring`) while every other form control here uses the brand
* `primary-30` — flagged as an inconsistency in the source's own style
* guide, so this bundle unifies on `primary-30` everywhere. The source
* depends on the `tailwindcss-animate` plugin for its open/close animation,
* which isn't part of this bundle, so `SelectContent` instead gets a plain
* CSS keyframe animation (`.animate-select-content` in globals.css) driven
* by Radix's `data-state` attribute. The chevron icons and SelectItem text
* are `text-foreground`, not the source's hardcoded `text-dark-70` — same
* reasoning as Input: near-black on a near-black dark-mode page/dropdown
* would be unreadable, and `text-foreground` is the same color in light
* mode anyway.
*/
export const Select = SelectPrimitive.Root;
export const SelectGroup = SelectPrimitive.Group;
export const SelectValue = SelectPrimitive.Value;
export const SelectTrigger = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
color?: string;
}
>(({ className, color, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"border-gray-70 flex w-full items-center justify-between rounded-[4px] border bg-transparent px-3 py-2 text-sm whitespace-nowrap",
"placeholder:text-muted focus:ring-primary-30 focus:ring-1 focus:outline-none",
"disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
color,
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown
className={cn("text-foreground h-4 w-4 opacity-50", color)}
/>
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
export const SelectScrollUpButton = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="text-foreground h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
export const SelectScrollDownButton = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="text-foreground h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
export const SelectContent = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"border-border bg-background text-foreground animate-select-content relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md",
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",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
export const SelectLabel = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
export const SelectItem = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"text-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none",
"focus:bg-accent/10 focus:text-accent",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
export const SelectSeparator = React.forwardRef<
React.ComponentRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;