Select
A select component with searchable options and custom styling
registry/preskok/ui/preskok-ui/select.tsx
"use client"
import { twJoin } from "cn"
import { ChevronsUpDownIcon } from "lucide-react"
import type {
ListBoxProps,
PopoverProps,
SelectProps as SelectPrimitiveProps,
} from "react-aria-components/Select"
import {
Button,
ListBox,
Select as SelectPrimitive,
SelectValue,
} from "react-aria-components/Select"
import { cx } from "@/lib/primitive"
import {
DropdownDescription,
DropdownItem,
DropdownLabel,
DropdownSection,
DropdownSeparator,
} from "./dropdown"
import { fieldStyles } from "./field"
import { PopoverContent } from "./popover"
interface SelectProps<
T extends object,
M extends "single" | "multiple" = "single",
> extends SelectPrimitiveProps<T, M> {
items?: Iterable<T, M>
}
const Select = <T extends object, M extends "single" | "multiple" = "single">({
className,
...props
}: SelectProps<T, M>) => {
return (
<SelectPrimitive
data-slot="control"
className={cx(fieldStyles({ className: "group/select" }), className)}
{...props}
/>
)
}
interface SelectListProps<T extends object> extends Omit<
ListBoxProps<T>,
"layout" | "orientation"
> {
items?: Iterable<T>
popover?: Omit<PopoverProps, "children">
}
const SelectContent = <T extends object>({
items,
className,
popover,
...props
}: SelectListProps<T>) => {
return (
<PopoverContent
placement={popover?.placement ?? "bottom"}
className={cx(
"min-w-(--trigger-width) scroll-py-1 overflow-y-auto overscroll-contain",
popover?.className
)}
{...popover}
>
<ListBox
layout="stack"
orientation="vertical"
className={cx(
"grid max-h-96 w-full grid-cols-[auto_1fr] flex-col gap-y-1 p-1 outline-hidden *:[[role='group']+[role=group]]:mt-4 *:[[role='group']+[role=separator]]:mt-1",
className
)}
items={items}
{...props}
/>
</PopoverContent>
)
}
interface SelectTriggerProps extends React.ComponentProps<typeof Button> {
prefix?: React.ReactNode
className?: string
}
const SelectTrigger = ({
children,
className,
...props
}: SelectTriggerProps) => {
return (
<span data-slot="control" className="relative block w-full">
<Button
className={cx(
"group/select-trigger border-input text-foreground flex w-full min-w-0 cursor-default items-center gap-x-2 rounded-lg border px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] text-start outline-hidden transition duration-200 sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)] sm:text-sm/6 sm:*:text-sm/6 dark:shadow-none",
"focus:border-ring/70 focus:ring-ring/20 focus:enabled:hover:border-ring/80 focus:ring-3",
"enabled:hover:border-muted-foreground/30",
"group-open/select:border-ring/70 group-open/select:ring-ring/20 group-open/select:ring-3",
"group-open/select:invalid:border-destructive/70 group-open/select:invalid:ring-destructive/20 group-invalid/select:border-destructive/70 group-invalid/select:ring-destructive/20 group-invalid/select:enabled:hover:border-destructive/80 group-focus/select:group-invalid/select:border-destructive/70 group-focus/select:group-invalid/select:ring-destructive/20 group-focus/select:group-invalid/select:enabled:hover:border-destructive/80 group-open/select:invalid:ring-3",
"*:data-[slot=icon]:size-5 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:self-center sm:*:data-[slot=icon]:size-4",
"*:data-[slot=loader]:size-5 *:data-[slot=loader]:shrink-0 *:data-[slot=loader]:self-center sm:*:data-[slot=loader]:size-4",
"forced-colors:group-invalid/select:border-[Mark] forced-colors:group-focus/select:border-[Highlight] forced-colors:group-focus/select:group-invalid/select:border-[Mark]",
"group-disabled/select:bg-muted group-disabled/select:opacity-50 forced-colors:group-disabled/select:border-[GrayText] forced-colors:group-disabled/select:text-[GrayText]",
"in-disabled:bg-muted in-disabled:opacity-50 forced-colors:in-disabled:border-[GrayText] forced-colors:in-disabled:text-[GrayText]",
"dark:scheme-dark",
className
)}
>
{(values) => (
<>
{props.prefix && (
<span className="text-muted-foreground">{props.prefix}</span>
)}
{typeof children === "function" ? children(values) : children}
{!children && (
<>
<SelectValue
data-slot="select-value"
className={twJoin([
"data-placeholder:text-muted-foreground truncate text-start sm:text-sm/6 [&_[slot=description]]:hidden",
"has-data-[slot=avatar]:grid has-data-[slot=avatar]:grid-cols-[1fr_auto] has-data-[slot=avatar]:items-center has-data-[slot=avatar]:gap-x-2",
"has-data-[slot=icon]:grid has-data-[slot=icon]:grid-cols-[1fr_auto] has-data-[slot=icon]:items-center has-data-[slot=icon]:gap-x-2",
"*:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:size-4",
"*:data-[slot=avatar]:[--avatar-size:--spacing(5)] sm:*:data-[slot=avatar]:[--avatar-size:--spacing(4.5)]",
])}
/>
<ChevronsUpDownIcon
data-slot="chevron"
className="-mr-1 ml-auto size-5 text-muted-foreground sm:size-4"
/>
</>
)}
</>
)}
</Button>
</span>
)
}
const SelectSection = DropdownSection
const SelectSeparator = DropdownSeparator
const SelectLabel = DropdownLabel
const SelectDescription = DropdownDescription
const SelectItem = DropdownItem
export {
Select,
SelectContent,
SelectDescription,
SelectItem,
SelectLabel,
SelectSection,
SelectSeparator,
SelectTrigger,
}
export type { SelectProps, SelectTriggerProps }
- Interaction
- dropdown
DependenciesBundle weight · light
- icons
- lucide-react