Select
Displays a list of options for the user to pick from, triggered by a button.
npx shadcn@latest add @velaui/selectpnpm dlx velaui add @velaui/selectyarn dlx shadcn@latest add @velaui/selectbunx --bun shadcn@latest add @velaui/selectveni add selectInstalls the copy of vela-ui that veni-builder serves to Venizia apps; veni-builder serves the Radix UI copy only, so this tab exists for that base alone.
npx shadcn@latest add @velaui/selectpnpm dlx velaui add @velaui/selectyarn dlx shadcn@latest add @velaui/selectbunx --bun shadcn@latest add @velaui/selectExamples
- Default
- With label
- With dot
- With icon
- With avatar
- Helper text
- Invalid
- Disabled
- Grouped
- Content position
Sizes
Usage
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function Example() {
return (
<div className="w-full max-w-xs">
<Select>
<SelectTrigger>
<SelectValue placeholder="Select item" />
</SelectTrigger>
<SelectContent>
<SelectItem value="linden">Linden Supply Co.</SelectItem>
<SelectItem value="harbor">Harbor & Pine</SelectItem>
<SelectItem value="tanaka">Tanaka Freight</SelectItem>
</SelectContent>
</Select>
</div>
);
}import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const ITEMS = {
linden: "Linden Supply Co.",
harbor: "Harbor & Pine",
tanaka: "Tanaka Freight",
};
export default function Example() {
return (
<div className="w-full max-w-xs">
<Select items={ITEMS}>
<SelectTrigger>
<SelectValue placeholder="Select item" />
</SelectTrigger>
<SelectContent>
<SelectItem value="linden">Linden Supply Co.</SelectItem>
<SelectItem value="harbor">Harbor & Pine</SelectItem>
<SelectItem value="tanaka">Tanaka Freight</SelectItem>
</SelectContent>
</Select>
</div>
);
}Props
Select
Takes every prop of SelectProps.
SelectContent
| Prop | Type | Default |
|---|---|---|
align | "center" | "start" | "end" | "center" |
position | "item-aligned" | "popper" | "item-aligned" |
Takes these and every prop of SelectContentProps.
SelectGroup
Takes every prop of SelectGroupProps.
SelectItem
Takes every prop of SelectItemProps.
SelectLabel
Takes every prop of SelectLabelProps.
SelectScrollDownButton
Takes every prop of SelectScrollDownButtonProps.
SelectScrollUpButton
Takes every prop of SelectScrollUpButtonProps.
SelectSeparator
Takes every prop of SelectSeparatorProps.
SelectTrigger
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "sm" |
variant | "fill" | "underline" | "outlined" | "outlined" |
Takes these and every prop of SelectTriggerProps.
SelectValue
Takes every prop of SelectValueProps.
SelectContent
| Prop | Type | Default |
|---|---|---|
align | import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").Align | "center" |
position | "item-aligned" | "popper" | "item-aligned" |
Takes these and every prop of SelectContentProps.
SelectGroup
Takes every prop of SelectGroupProps.
SelectItem
Takes every prop of SelectItemProps.
SelectLabel
Takes every prop of SelectLabelProps.
SelectScrollDownButton
Takes every prop of SelectScrollDownButtonProps.
SelectScrollUpButton
Takes every prop of SelectScrollUpButtonProps.
SelectSeparator
Takes every prop of SelectSeparatorProps.
SelectTrigger
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "sm" |
variant | "underline" | "fill" | "outlined" | "outlined" |
Takes these and every prop of SelectTriggerProps.
SelectValue
Takes every prop of SelectValueProps.
Dependencies
- npm
class-variance-authority@0.7.1radix-ui@1.6.0- Installs with it
- 3 icons
- npm
@base-ui/react@1.6.0class-variance-authority@0.7.1- Installs with it
- 3 icons
Source
components/ui/select.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Select as SelectPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";
import { ChevronDown } from "@/components/icons/chevron-down";
import { Check01 } from "@/components/icons/check-01";
import { ChevronUp } from "@/components/icons/chevron-up";
import { cn } from "@/lib/utils";
const selectTriggerVariants = cva(
"flex w-full items-center justify-between gap-2 text-sm leading-[1.4] text-text-strong whitespace-nowrap transition-colors outline-none select-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-text-disabled data-placeholder:text-text-note *: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]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3",
{
variants: {
variant: {
outlined:
"rounded-md border border-border-default bg-bg-main hover:border-border-hover hover:bg-input-hover focus-visible:border-ring-neutral-outline focus-visible:bg-input-default focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow disabled:border-border-disabled disabled:bg-input-disabled aria-invalid:border-negative-middle aria-invalid:focus-visible:border-negative-middle aria-invalid:focus-visible:ring-negative-middle/25",
// Fill draws no border at rest, but hover/focus/invalid all draw a 1px
// one — reserve it as transparent so entering a state never reflows.
fill: "rounded-md border border-transparent bg-input-default hover:border-border-hover hover:bg-input-hover focus-visible:border-ring-neutral-outline focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow disabled:border-transparent disabled:bg-input-disabled aria-invalid:border-negative-middle aria-invalid:focus-visible:border-negative-middle aria-invalid:focus-visible:ring-negative-middle/25",
// Underline carries no surface, no radius and no focus ring — the whole
// state vocabulary lives on the bottom border.
underline:
"rounded-none border-0 border-b border-b-border-default bg-transparent hover:border-b-border-active focus-visible:border-b-border-active disabled:border-b-border-default disabled:bg-transparent aria-invalid:border-b-negative-middle aria-invalid:focus-visible:border-b-negative-middle",
},
size: {
sm: "h-8 px-2.5 py-1.5",
md: "h-10 px-3 py-2.5",
lg: "h-12 px-4 py-3.5",
},
},
compoundVariants: [
{
variant: "underline",
size: ["sm", "md", "lg"],
className: "px-0",
},
],
defaultVariants: {
variant: "outlined",
size: "sm",
},
},
);
export type SelectProps = React.ComponentProps<typeof SelectPrimitive.Root>;
function Select({
...props
}: SelectProps) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
export type SelectGroupProps = React.ComponentProps<typeof SelectPrimitive.Group>;
function SelectGroup({
className,
...props
}: SelectGroupProps) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("flex scroll-my-1 flex-col gap-1", className)}
{...props}
/>
);
}
export type SelectValueProps = React.ComponentProps<typeof SelectPrimitive.Value>;
function SelectValue({
...props
}: SelectValueProps) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
export type SelectTriggerProps = React.ComponentProps<
typeof SelectPrimitive.Trigger
> &
VariantProps<typeof selectTriggerVariants>;
function SelectTrigger({
className,
variant = "outlined",
size = "sm",
children,
...props
}: SelectTriggerProps) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-variant={variant}
data-size={size}
className={cn(selectTriggerVariants({ variant, size }), className)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="pointer-events-none size-4 text-icon-muted" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
export type SelectContentProps = React.ComponentProps<typeof SelectPrimitive.Content>;
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: SelectContentProps) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn(
"relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border-default bg-bg-main p-1 text-text-strong shadow-md duration-100 data-[align-trigger=true]:animate-none 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
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}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
"flex flex-col gap-1 data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
export type SelectLabelProps = React.ComponentProps<typeof SelectPrimitive.Label>;
function SelectLabel({
className,
...props
}: SelectLabelProps) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-2 py-1 text-xs text-text-muted", className)}
{...props}
/>
);
}
export type SelectItemProps = React.ComponentProps<typeof SelectPrimitive.Item>;
function SelectItem({
className,
children,
...props
}: SelectItemProps) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-10 pl-2 text-sm leading-[1.4] text-text-strong outline-hidden select-none focus:bg-bg-surface-1 data-[state=checked]:bg-bg-surface-1 data-disabled:pointer-events-none data-disabled:text-text-disabled [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check01 className="pointer-events-none text-icon-strong size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
export type SelectSeparatorProps = React.ComponentProps<typeof SelectPrimitive.Separator>;
function SelectSeparator({
className,
...props
}: SelectSeparatorProps) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn(
"pointer-events-none -mx-1 my-1 h-px bg-border-default",
className,
)}
{...props}
/>
);
}
export type SelectScrollUpButtonProps = React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>;
function SelectScrollUpButton({
className,
...props
}: SelectScrollUpButtonProps) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-bg-main py-1 text-icon-muted [&_svg:not([class*='size-'])]:size-3",
className,
)}
{...props}
>
<ChevronUp />
</SelectPrimitive.ScrollUpButton>
);
}
export type SelectScrollDownButtonProps = React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>;
function SelectScrollDownButton({
className,
...props
}: SelectScrollDownButtonProps) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-bg-main py-1 text-icon-muted [&_svg:not([class*='size-'])]:size-3",
className,
)}
{...props}
>
<ChevronDown />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
selectTriggerVariants,
};components/ui/select.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { cva, type VariantProps } from "class-variance-authority";
import { ChevronDown } from "@/components/icons/chevron-down";
import { Check01 } from "@/components/icons/check-01";
import { ChevronUp } from "@/components/icons/chevron-up";
import { cn } from "@/lib/utils";
const selectTriggerVariants = cva(
"flex w-full items-center justify-between gap-2 text-sm leading-[1.4] text-text-strong whitespace-nowrap transition-colors outline-none select-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-text-disabled data-placeholder:text-text-note *: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]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3",
{
variants: {
variant: {
outlined:
"rounded-md border border-border-default bg-bg-main hover:border-border-hover hover:bg-input-hover focus-visible:border-ring-neutral-outline focus-visible:bg-input-default focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow disabled:border-border-disabled disabled:bg-input-disabled aria-invalid:border-negative-middle aria-invalid:focus-visible:border-negative-middle aria-invalid:focus-visible:ring-negative-middle/25",
// Fill draws no border at rest, but hover/focus/invalid all draw a 1px
// one — reserve it as transparent so entering a state never reflows.
fill: "rounded-md border border-transparent bg-input-default hover:border-border-hover hover:bg-input-hover focus-visible:border-ring-neutral-outline focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow disabled:border-transparent disabled:bg-input-disabled aria-invalid:border-negative-middle aria-invalid:focus-visible:border-negative-middle aria-invalid:focus-visible:ring-negative-middle/25",
// Underline carries no surface, no radius and no focus ring — the whole
// state vocabulary lives on the bottom border.
underline:
"rounded-none border-0 border-b border-b-border-default bg-transparent hover:border-b-border-active focus-visible:border-b-border-active disabled:border-b-border-default disabled:bg-transparent aria-invalid:border-b-negative-middle aria-invalid:focus-visible:border-b-negative-middle",
},
size: {
sm: "h-8 px-2.5 py-1.5",
md: "h-10 px-3 py-2.5",
lg: "h-12 px-4 py-3.5",
},
},
compoundVariants: [
{
variant: "underline",
size: ["sm", "md", "lg"],
className: "px-0",
},
],
defaultVariants: {
variant: "outlined",
size: "sm",
},
},
);
export type SelectProps = React.ComponentProps<typeof SelectPrimitive.Root>;
const Select = SelectPrimitive.Root;
export type SelectGroupProps = React.ComponentProps<typeof SelectPrimitive.Group>;
function SelectGroup({
className,
...props
}: SelectGroupProps) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("flex scroll-my-1 flex-col gap-1", className)}
{...props}
/>
);
}
export type SelectValueProps = React.ComponentProps<typeof SelectPrimitive.Value>;
function SelectValue({
...props
}: SelectValueProps) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
export type SelectTriggerProps = React.ComponentProps<
typeof SelectPrimitive.Trigger
> &
VariantProps<typeof selectTriggerVariants>;
function SelectTrigger({
className,
variant = "outlined",
size = "sm",
children,
...props
}: SelectTriggerProps) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-variant={variant}
data-size={size}
className={cn(selectTriggerVariants({ variant, size }), className)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDown className="pointer-events-none size-4 text-icon-muted" />
}
/>
</SelectPrimitive.Trigger>
);
}
export type SelectContentProps = React.ComponentProps<typeof SelectPrimitive.Popup> &
Pick<
React.ComponentProps<typeof SelectPrimitive.Positioner>,
"align" | "alignOffset" | "side" | "sideOffset"
> & {
position?: "item-aligned" | "popper";
};
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: SelectContentProps) {
const alignItemWithTrigger = position === "item-aligned";
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
align={align}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn(
"relative z-50 max-h-(--available-height) min-w-[max(--spacing(36),var(--anchor-width))] origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border-default bg-bg-main p-1 text-text-strong shadow-md duration-100 data-closed:overflow-hidden data-[align-trigger=true]:animate-none 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
!alignItemWithTrigger &&
"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,
)}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List className="flex flex-col gap-1">{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
);
}
export type SelectLabelProps = React.ComponentProps<typeof SelectPrimitive.GroupLabel>;
function SelectLabel({
className,
...props
}: SelectLabelProps) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-2 py-1 text-xs text-text-muted", className)}
{...props}
/>
);
}
export type SelectItemProps = React.ComponentProps<typeof SelectPrimitive.Item>;
function SelectItem({
className,
children,
...props
}: SelectItemProps) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-10 pl-2 text-sm leading-[1.4] text-text-strong outline-hidden select-none data-selected:bg-bg-surface-1 data-disabled:pointer-events-none data-disabled:text-text-disabled [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
className,
)}
{...props}
>
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check01 className="pointer-events-none text-icon-strong size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText className="flex items-center gap-2">
{children}
</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
export type SelectSeparatorProps = React.ComponentProps<typeof SelectPrimitive.Separator>;
function SelectSeparator({
className,
...props
}: SelectSeparatorProps) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn(
"pointer-events-none -mx-1 my-1 h-px bg-border-default",
className,
)}
{...props}
/>
);
}
export type SelectScrollUpButtonProps = React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>;
function SelectScrollUpButton({
className,
...props
}: SelectScrollUpButtonProps) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-bg-main py-1 text-icon-muted [&_svg:not([class*='size-'])]:size-3",
className,
)}
{...props}
>
<ChevronUp />
</SelectPrimitive.ScrollUpArrow>
);
}
export type SelectScrollDownButtonProps = React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>;
function SelectScrollDownButton({
className,
...props
}: SelectScrollDownButtonProps) {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-bg-main py-1 text-icon-muted [&_svg:not([class*='size-'])]:size-3",
className,
)}
{...props}
>
<ChevronDown />
</SelectPrimitive.ScrollDownArrow>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
selectTriggerVariants,
};