Docs
Select

Select

A Select element that allows a user to select from a list of options.

Installation

Copy and paste the following code into your component.

"use client";
import React, {
  createContext,
  useContext,
  useState,
  useRef,
  useEffect,
} from "react";
import { Button, ButtonProps } from "@/components/ui/button/button";
import styles from "./select.module.css";
import { useRefs } from "@/components/lib/useRefs";

/* -------------------------------------------------------------------------------------------------
 * SELECT CONTEXT
 * -----------------------------------------------------------------------------------------------*/

type Option = {
  value: string;
  display: string;
};

type SelectContextValue = {
  open: boolean;
  selectedItem: string | null;
  options: Option[];
  triggerRef: React.RefObject<HTMLButtonElement>;
  contentRef: React.RefObject<HTMLDivElement>;
  onOpenChange(open: boolean): void;
  onOpenToggle(): void;
  onSelectItem(value: string): void;
  registerOption(option: Option): void;
};

const SelectContext = createContext<SelectContextValue | undefined>(undefined);

interface SelectProps {
  value: string;
  onSelectItem: (value: string) => void;
  children: React.ReactNode;
}

const Select: React.FC<SelectProps> = ({
  value,
  onSelectItem,
  children,
}: SelectProps) => {
  const [open, setOpen] = useState(false);
  const [options, setOptions] = useState<Option[]>([]);
  const triggerRef = useRef<HTMLButtonElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);

  const onOpenToggle = () => setOpen((prevOpen) => !prevOpen);

  const registerOption = (option: Option) => {
    setOptions((prevOptions) => [...prevOptions, option]);
  };

  const selectedItem = options.find((option) => option.value === value);

  const contextValue: SelectContextValue = {
    contentRef,
    triggerRef,
    open,
    selectedItem: value ?? selectedItem, // Current selected value
    onOpenChange: setOpen,
    onOpenToggle,
    onSelectItem: (selectedValue: string) => {
      onSelectItem(selectedValue);
      setOpen(false); // Close the dropdown after selection
    },
    options,
    registerOption,
  };

  return (
    <SelectContext.Provider value={contextValue}>
      {children}
    </SelectContext.Provider>
  );
};

const useSelect = () => {
  const context = useContext(SelectContext);
  if (!context) {
    throw new Error("useSelect must be used within a SelectProvider");
  }
  return context;
};

/* -------------------------------------------------------------------------------------------------
 * SELECT TRIGGER
 * -----------------------------------------------------------------------------------------------*/

const TRIGGER_NAME = "SelectTrigger";

type SelectTriggerElement = React.ElementRef<typeof Button>;

interface SelectTriggerProps extends ButtonProps {
  placeholder?: string;
}

const SelectTrigger = React.forwardRef<
  SelectTriggerElement,
  SelectTriggerProps
>((props, ref) => {
  const { open, onOpenToggle, selectedItem, triggerRef, options } = useSelect();
  const combinedRefs = useRefs(ref, triggerRef);

  const selectedOption = options.find(
    (option) => option.value === selectedItem
  );

  const defaultPlaceholder = props.placeholder ?? "Select an option";

  return (
    <button
      type='button'
      aria-haspopup='dialog'
      aria-expanded={open}
      aria-controls='Dialog'
      className={styles.trigger}
      onClick={onOpenToggle}
      ref={combinedRefs}
      {...props}>
      {selectedOption ? selectedOption.display : defaultPlaceholder}
    </button>
  );
});

SelectTrigger.displayName = TRIGGER_NAME;

/* -------------------------------------------------------------------------------------------------
 * SELECT CONTENT
 * -----------------------------------------------------------------------------------------------*/

const CONTENT_NAME = "SelectContent";

const SelectContent: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const { open, contentRef } = useSelect();

  return open ? (
    <div
      ref={contentRef}
      id='select-content'
      role='listbox'
      aria-labelledby='select-trigger'
      className={styles.content}>
      {children}
    </div>
  ) : null;
};

SelectContent.displayName = CONTENT_NAME;

/* -------------------------------------------------------------------------------------------------
 * SELECT ITEM
 * -----------------------------------------------------------------------------------------------*/

const ITEM_NAME = "SelectItem";

interface SelectItemProps {
  value: string;
  display: string;
}

const SelectItem: React.FC<SelectItemProps> = ({ value, display }) => {
  const { onSelectItem, selectedItem, registerOption } = useSelect();
  const isSelected = selectedItem === value;

  useEffect(() => {
    registerOption({ value, display });
  }, [value, display]);

  return (
    <div
      role='option'
      aria-selected={isSelected}
      tabIndex={0}
      onClick={() => onSelectItem(value)}
      className={`${styles.option} ${isSelected ? styles.selected : ""}`}
      onKeyDown={(e) => {
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          onSelectItem(value);
        }
      }}>
      {display}
    </div>
  );
};

SelectItem.displayName = ITEM_NAME;

export { SelectTrigger, SelectItem, SelectContent };
export { Select, useSelect };

Copy and paste the following CSS into a .module.css file.

.trigger {
  align-items: center;
  background-color: transparent;
  border: 1px solid hsl(var(--input));
  border-radius: calc(var(--radius) - 2px);
  color: hsl(var(--foreground));
  display: flex;
  font-size: 0.875rem;
  height: 2.5rem;
  max-width: 20rem;
  outline-offset: 2px;
  padding-inline: 0.5rem;
  padding-block: 0.75rem;
  width: 100%;
}

.trigger:hover {
  background: transparent;
}

.trigger:focus {
  border-color: var(--secondary);
  outline: none;
}

/* Style for the dropdown content */
.content {
  background: hsl(var(--background));
  border: 1px solid hsl(var(--input));
  border-radius: calc(var(--radius) - 2px);
  color: hsl(var(--foreground));
  font-size: 0.875rem;
  width: -webkit-fill-available;
  max-width: 20rem;
  outline-offset: 2px;
  padding-inline: 0.5rem;
  padding-block: 0.75rem;
  overflow-y: auto;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  position: absolute;
  z-index: 1000;
}

/* Style for each item in the dropdown */
.option {
  color: hsl(var(--foreground));
  font-size: 0.875rem;
  padding-inline: 0.5rem;
  padding-block: 0.75rem;
  cursor: pointer;
  transition: background-color 0.2s;
}

/* Hover effect for dropdown items */
.option:hover {
  background-color: hsl(var(--muted));
}

/* Selected item styling */
.selected {
  background-color: hsl(var(--muted));
  font-weight: var(--bold);
}

Update the import paths to match your project setup.

Usage

import { Switch } from "@/components/ui/select";
<Select value={value} onSelectItem={(option: string) => setValue(option)}>
  <SelectTrigger placeholder='Select an option' />
  <SelectContent>
    <SelectItem value='opt1' display='Option 1' />
    <SelectItem value='opt2' display='Option 2' />
    <SelectItem value='opt3' display='Option 3' />
  </SelectContent>
</Select>

Examples

Form Select

This is the name of your project.

What framework would you like to write tests with