shadimage

Select

A dropdown control for choosing a single value from a list of options.

ui/select.tsx

"use client";

import { Select as SelectPrimitive } from "@base-ui/react/select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "blode-icons-react";
import type * as React from "react";

import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";

type SelectProps<Value = string> = Omit<SelectPrimitive.Root.Props<Value>, "onValueChange"> & {
  onValueChange?: (value: Value, eventDetails: SelectPrimitive.Root.ChangeEventDetails) => void;
};

const Select = <Value = string,>({ onValueChange, ...props }: SelectProps<Value>) => (
  <SelectPrimitive.Root
    data-slot="select"
    onValueChange={(value, eventDetails) => {
      if (value !== null) {
        onValueChange?.(value as Value, eventDetails);
      }
    }}
    {...props}
  />
);

const SelectGroup = ({ ...props }: SelectPrimitive.Group.Props) => (
  <SelectPrimitive.Group data-slot="select-group" {...props} />
);

const SelectValue = ({ ...props }: SelectPrimitive.Value.Props) => (
  <SelectPrimitive.Value data-slot="select-value" {...props} />
);

const SelectTrigger = ({
  className,
  size = "default",
  children,
  ...props
}: SelectPrimitive.Trigger.Props & {
  size?: "sm" | "default";
}) => (
  <SelectPrimitive.Trigger
    className={cn(
      buttonVariants({
        size: size === "sm" ? "input-sm" : "input",
        variant: "input",
      }),
      "w-full justify-between whitespace-normal pr-3",
      className,
    )}
    data-slot="select-trigger"
    {...props}
  >
    <span className="line-clamp-2 flex-1 pr-0.5 text-left">{children}</span>
    <SelectPrimitive.Icon render={<ChevronDownIcon className="size-4 opacity-50" />} />
  </SelectPrimitive.Trigger>
);

const SelectScrollUpButton = ({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) => (
  <SelectPrimitive.ScrollUpArrow
    className={cn("flex cursor-default items-center justify-center py-1", className)}
    data-slot="select-scroll-up-button"
    {...props}
  >
    <ChevronUpIcon className="size-4" />
  </SelectPrimitive.ScrollUpArrow>
);

const SelectScrollDownButton = ({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) => (
  <SelectPrimitive.ScrollDownArrow
    className={cn("flex cursor-default items-center justify-center py-1", className)}
    data-slot="select-scroll-down-button"
    {...props}
  >
    <ChevronDownIcon className="size-4" />
  </SelectPrimitive.ScrollDownArrow>
);

const SelectContent = ({
  className,
  children,
  position = "item-aligned",
  side = "bottom",
  sideOffset = 4,
  align = "center",
  alignOffset = 0,
  alignItemWithTrigger,
  ...props
}: SelectPrimitive.Popup.Props &
  Pick<
    SelectPrimitive.Positioner.Props,
    "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
  > & {
    position?: "item-aligned" | "popper";
  }) => {
  const shouldAlignItemWithTrigger = alignItemWithTrigger ?? position !== "popper";

  return (
    <SelectPrimitive.Portal>
      <SelectPrimitive.Positioner
        align={align}
        alignItemWithTrigger={shouldAlignItemWithTrigger}
        alignOffset={alignOffset}
        className="isolate z-110"
        side={side}
        sideOffset={sideOffset}
      >
        <SelectPrimitive.Popup
          className={cn(
            "data-open:fade-in-80 relative z-110 min-w-[8rem] origin-(--transform-origin) overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-soft data-closed:animate-out data-open:animate-in",
            position === "popper" && "translate-y-1",
            className,
          )}
          data-slot="select-content"
          {...props}
        >
          <SelectScrollUpButton />
          <SelectPrimitive.List
            className={cn(
              "scroll-fade max-h-(--available-height) overflow-x-hidden overflow-y-auto p-1",
              position === "popper" && "w-full min-w-(--anchor-width) scroll-my-1",
            )}
          >
            {children}
          </SelectPrimitive.List>
          <SelectScrollDownButton />
        </SelectPrimitive.Popup>
      </SelectPrimitive.Positioner>
    </SelectPrimitive.Portal>
  );
};

const SelectLabel = ({ className, ...props }: SelectPrimitive.GroupLabel.Props) => (
  <SelectPrimitive.GroupLabel
    className={cn("py-1.5 pr-8 pl-2 font-semibold", className)}
    data-slot="select-label"
    {...props}
  />
);

const SelectItem = ({ className, children, ...props }: SelectPrimitive.Item.Props) => (
  <SelectPrimitive.Item
    className={cn(
      "relative flex w-full cursor-default select-none items-center rounded-lg py-1.5 pr-8 pl-2 font-sans text-base leading-snug outline-hidden focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
      className,
    )}
    data-slot="select-item"
    {...props}
  >
    <span
      className="absolute right-2 flex size-3.5 items-center justify-center"
      data-slot="select-item-indicator"
    >
      <SelectPrimitive.ItemIndicator>
        <CheckIcon className="size-4" />
      </SelectPrimitive.ItemIndicator>
    </span>
    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
);

const SelectSeparator = ({ className, ...props }: SelectPrimitive.Separator.Props) => (
  <SelectPrimitive.Separator
    className={cn("mx-3 my-1 h-px bg-input", className)}
    data-slot="select-separator"
    {...props}
  />
);

export {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectScrollDownButton,
  SelectScrollUpButton,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
};
Interaction
dropdown

DependenciesBundle weight · light

ui-primitives
@base-ui/react