Select

Displays a list of options for the user to pick from—triggered by a button.

import Select from "@tailus-ui/Select";

const countries = [
  { flag: "🇨🇩", name: "DR Congo" },
  { flag: "🇨🇬", name: "Congo Braza" },
  { flag: "🇦🇴", name: "Angola" },
  { flag: "🇫🇷", name: "France" },
  { flag: "🇬🇧", name: "United Kingdom" },
  { flag: "🇪🇸", name: "Spain" },
];

export const Overview = () => (
    <Select.Root defaultValue="DR Congo">
        <Select.Trigger size="md" className="w-56">
            <Select.Value placeholder="Role" />
            <Select.Icon />
        </Select.Trigger>

        <Select.Portal>
            <Select.Content mixed className="z-50">
                <Select.Viewport>
                    {
                        countries.map((country) => (
                            <SelectItem entry={country} key={country.name} />
                        ))
                    }
                </Select.Viewport>
            </Select.Content>
        </Select.Portal>
    </Select.Root>
)

type Entry = {
    flag: string;
    name: string;
}

const SelectItem = ({ entry }: { entry: Entry }) => {
  return (
    <Select.Item value={entry.name} className="pl-7 items-center">
      <Select.ItemIndicator />
      <Select.ItemText>
        <span role="img" aria-label={entry.name} className="mr-2">
          {entry.flag}
        </span>
        {entry.name}
      </Select.ItemText>
    </Select.Item>
  );
};

Installation

Install the primitive and Copy-Paste the component code in a .tsx file.

npm install @radix-ui/react-select
import React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { cloneElement } from "@lib/utils.ts";

import {
  caption,
  select,
  trigger,
  type SelectProps,
  type TriggerProps,
  type SeparatorProps
} from "@tailus/themer"

import { twMerge } from "tailwind-merge";
import { ChevronDown, ChevronUp } from "lucide-react";

interface SelectIconProps {
  className?: string,
  children: React.ReactNode,
  size?: TriggerProps["size"]
}

const SelectGroup = SelectPrimitive.Group;

const SelectContext = React.createContext<SelectProps>({});
const { button, separator, itemIndicator, label } = select.soft()

const SelectScrollUpButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({className, children, ...props}, forwardedRef) => (
  <SelectPrimitive.ScrollUpButton
    {...props}
    ref={forwardedRef}
    className={button({className})}
  >
    {children || <ChevronUp className="size-3"/>}
  </SelectPrimitive.ScrollUpButton>
));

const SelectScrollDownButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({className, children, ...props}, forwardedRef) => (
  <SelectPrimitive.ScrollDownButton
    {...props}
    ref={forwardedRef}
    className={button({className})}
  >
    {children || <ChevronDown className="size-3"/>}
  </SelectPrimitive.ScrollDownButton>
));

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & TriggerProps
>(({size="md", variant="mixed", className, children, ...props}, forwardedRef) => {
  const { parent } = trigger()
  return (
    <SelectPrimitive.Trigger
      {...props}
      ref={forwardedRef}
      className={parent({size, variant, className})}
    >
      {children}
    </SelectPrimitive.Trigger>
  )
});

const SelectTriggerIcon = ({ className, size, children }: SelectIconProps) => {
  const {icon} = trigger()
  return cloneElement(children as React.ReactElement, icon({size, className}));
};

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> & SelectProps
  >(({ className, variant="solid", intent="primary", mixed=false, fancy=false, children, ...props }, forwardedRef) => {

    const {
      variant: contextVariant,
      intent: contextIntent,
      fancy: contextFancy,
      mixed: contextMixed,
    } = React.useContext(SelectContext);

    variant = variant || contextVariant || "solid";
    intent = intent || contextIntent || "primary";
    fancy = fancy || contextFancy || false;
    mixed = mixed || contextMixed || false;

    if (fancy && mixed) {
      throw new Error('The fancy and mixed props cannot be used together.');
    }
    
    const { content } = select[variant]();

    return (
      <SelectContext.Provider value={{variant, fancy, mixed, intent}}>
        <SelectPrimitive.Content
          {...props}
          ref={forwardedRef}
          className={content({ mixed, fancy, intent, className })}
        >
            {children}
        </SelectPrimitive.Content>
      </SelectContext.Provider>
    ); 
  });

const SelectItemIndicator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ItemIndicator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ItemIndicator>
>(({className, ...props}, forwardedRef) => (
  <SelectPrimitive.ItemIndicator
    {...props}
    ref={forwardedRef}
    className={itemIndicator({className})}
  />
));

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & SelectProps
  >(({ className, variant, children, ...props }, forwardedRef) => {

    const {
      variant : contextVariant,
      intent,
    } = React.useContext(SelectContext);

    variant = contextVariant || "solid";

    const { item } = select[variant]();

    return(
      <SelectPrimitive.Item
        {...props}
        ref={forwardedRef}
        className={item({ intent, className })}
      >
        {children}
      </SelectPrimitive.Item>
    )
  });

const SelectLabel = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({className, ...props}, forwardedRef) => (
  <SelectPrimitive.Label
    {...props}
    ref={forwardedRef}
    className={twMerge(caption(), label(), className)}
  />
));

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> & Pick<SeparatorProps, "fancy" | "dashed">
  >(({ className, fancy=false, dashed=false, ...props }, forwardedRef) => {

    const {
        fancy : contextFancy,
    } = React.useContext(SelectContext);

    fancy = fancy || contextFancy || false;
    return (
      <SelectPrimitive.Separator
        {...props}
        ref={forwardedRef}
        className={separator({ fancy, dashed, className })}
      />
    );
  });


export default {
  Root: SelectPrimitive.Root,
  Trigger: SelectTrigger,
  Content: SelectContent,
  Item: SelectItem,
  Group: SelectGroup,
  Separator: SelectSeparator,
  Portal: SelectPrimitive.Portal,
  Value: SelectPrimitive.Value,
  Icon: SelectPrimitive.Icon,
  ItemIndicator: SelectItemIndicator,
  ScrollUpButton : SelectScrollUpButton,
  ScrollDownButton: SelectScrollDownButton,
  Label: SelectLabel,
  Viewport: SelectPrimitive.Viewport,
  ItemText: SelectPrimitive.ItemText,
  TriggerIcon : SelectTriggerIcon
}

Usage

Import all the parts and build your Select.

import Select from "@tailus-ui/Select";

const countries = [
  { flag: "🇨🇩", name: "DR Congo" },
  { flag: "🇨🇬", name: "Congo Braza" },
  { flag: "🇦🇴", name: "Angola" },
  { flag: "🇫🇷", name: "France" },
  { flag: "🇬🇧", name: "United Kingdom" },
  { flag: "🇪🇸", name: "Spain" },
];

export const Overview = () => (
    <Select.Root defaultValue="DR Congo">
        <Select.Trigger size="md" className="w-56">
            <Select.Value placeholder="Role" />
            <Select.Icon />
        </Select.Trigger>

        <Select.Portal>
            <Select.Content mixed className="z-50">
                <Select.Viewport>
                    {
                        countries.map((country) => (
                            <SelectItem entry={country} key={country.name} />
                        ))
                    }
                </Select.Viewport>
            </Select.Content>
        </Select.Portal>
    </Select.Root>
)

type Entry = {
    flag: string;
    name: string;
}

const SelectItem = ({ entry }: { entry: Entry }) => {
  return (
    <Select.Item value={entry.name} className="pl-7 items-center">
      <Select.ItemIndicator />
      <Select.ItemText>
        <span role="img" aria-label={entry.name} className="mr-2">
          {entry.flag}
        </span>
        {entry.name}
      </Select.ItemText>
    </Select.Item>
  );
};

Reference

Trigger

The trigger of the Select component

Prop
Type
Default
variant
enum
mixed
size
enum
md

Content

The content of the Select component

Prop
Type
Default
mixed
boolean
false
fancy
boolean
false
variant
enum
solid
intent
enum
primary

Item

The item of the Select component, which is the option. By default, its appearance (defined by variant and intent props) is determined by the parent Select.Content component.

Prop
Type
Default
variant
enum
solid
intent
enum
primary

Icon

The icon of the Select.Trigger component

Prop
Type
Default
size
enum
md

Item Trigger

The icon to indicate the selected item

Prop
Type
Default
size
enum
sm

Examples