Select

A sleek Select component designed with TailwindCSS for clear, responsive, and user-friendly dropdown menus.

Example

example.tsx

"use client"
import React, { useState, useRef, useEffect } from 'react';

type Option = {
    value : string,
    label : string
}

// Options as props or fetch
const options : Option[] = [
    {value : "jsx" , label : "JSX"},
    {value : "tsx" , label : "TSX"},
    {value : "html" , label : "HTML"},
    {value : "vue" , label : "Vue"},
];

const onSelect = (option : Option) => {
    // Display the selected option
}

const Select = () => {

  const [isOpen, setIsOpen] = useState(false);
  const [selectedOption, setSelectedOption] = useState<Option | null>(null);
  const [dropUp, setDropUp] = useState(false);
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
  const dropdownRef = useRef<HTMLDivElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);

  const handleOptionClick = (option : Option) => {
    setSelectedOption(option);
    onSelect(option);
    setIsOpen(false);
    setHighlightedIndex(-1);
  };

  const handleClickOutside = (e : MouseEvent) => {
    if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node) &&
        buttonRef.current && !buttonRef.current.contains(e.target as Node)) {
      setIsOpen(false);
      setHighlightedIndex(-1);
    }
  };

  const checkDropdownPosition = () => {
    if (buttonRef.current && dropdownRef.current) {
      const buttonRect = buttonRef.current.getBoundingClientRect();
      const dropdownHeight = dropdownRef.current.getBoundingClientRect().height;
      const spaceBelow = window.innerHeight - buttonRect.bottom;
      const spaceAbove = buttonRect.top;

      setDropUp(dropdownHeight > spaceBelow && spaceAbove > spaceBelow);
    }
  };

  const handleKeyDown = (e : globalThis.KeyboardEvent) => {
    e.preventDefault();
    if (!isOpen) return;

    switch (e.key) {
      case 'ArrowDown':
        setHighlightedIndex((prevIndex) =>
          prevIndex === options.length - 1 ? 0 : prevIndex + 1
        );
        break;
      case 'ArrowUp':
        setHighlightedIndex((prevIndex) =>
          prevIndex === 0 ? options.length - 1 : prevIndex - 1
        );
        break;
      case 'Enter':
        if (highlightedIndex >= 0) {
          handleOptionClick(options[highlightedIndex]);
        }
        break;
      case 'Escape':
        setIsOpen(false);
        setHighlightedIndex(-1);
        break;
      default:
        break;
    }
  };

  useEffect(() => {
    document.addEventListener('mousedown', handleClickOutside);
    window.addEventListener('resize', checkDropdownPosition);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
      window.removeEventListener('resize', checkDropdownPosition);
    };
  }, []);

  useEffect(() => {
    if (isOpen) {
      checkDropdownPosition();
    }
  }, [isOpen]);

  useEffect(() => {
    if (isOpen) {
      document.addEventListener("keydown" , handleKeyDown);
    } else {
      document.removeEventListener('keydown', handleKeyDown);
    }
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, [isOpen, highlightedIndex]);

  return (
    <div className="relative inline-block text-left w-64">
      <div>
        <button
          ref={buttonRef}
          type="button"
          className="text-[#E9EEF2] bg-[#161616] items-center inline-flex justify-between w-full rounded-md border border-gray-300 shadow-sm px-4 py-2  text-sm font-medium  hover:bg-[#191919]"
          onClick={() => setIsOpen(!isOpen)}
        >
          {selectedOption ? selectedOption.label : 'Select an option'}
          <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M8.25 15L12 18.75L15.75 15M8.25 9L12 5.25L15.75 9" stroke="#E9EEF2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </button>
      </div>
      {isOpen && (
        <div
          ref={dropdownRef}
          className={`bg-[#161616] origin-top-right absolute mt-2 w-full rounded-md shadow-lg ${dropUp ? 'bottom-full mb-2' : 'top-full mt-2'}`}
        >
          <div className="py-1">
            {options.map((option, index) => (
              <button
                key={option.value}
                className={`block px-4 py-2 text-sm w-full text-left ${highlightedIndex === index ? 'bg-gray-200 text-black' : 'hover:bg-[#191919] text-[#E9EEF2]'}`}
                onClick={() => handleOptionClick(option)}
              >
                {option.label}
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

export default Select;