A better <select> for React.js

I needed more flexibility when I was styling a <select> for a project some time ago. There are not many CSS rules that apply to the drop down part of it, so I figured I'd find a React component that mimics it. I looked at react-select but it was utterly over-engineered.

This made it nigh impossible to change the height. I just needed a few CSS rules on a typical <select> component, so I wrote my own. It's not in NPM yet, but it might get there some day.

It's just a root element with two children, one for the currently selected item and the collapsed state and one for the drop down. When you hit anywhere outside the drop down, it gets closed by a useEffect event listener on the document. That's all there is to it.


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

export interface Option {
  label: string;
  value: any;
}

export interface SelectProps extends React.HTMLAttributes<HTMLElement> {
  value: any;
  children: Option[];
  setValue: (arg0: any) => void;
}

const Select = (props: SelectProps) => {
  const [isOpen, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  // Create listener for when the user clicks somewhere outside the drop down
  useEffect(() => {
    const listener = (e: MouseEvent) => {
      if (e.target instanceof HTMLElement) {
        let p: HTMLElement | null = e.target;
        while (p) {
          if (ref.current === p) {
            // We clicked inside ourselves
            return;
          }
          p = p.parentElement;
        }
      }
      // We clicked outside
      setOpen(false);
    };
    document.addEventListener("mousedown", listener);
    return () => document.removeEventListener("mousedown", listener);
  }, []);

  return (
    <div
      className={`Select${isOpen ? " open" : ""}${
        props.className ? ` ${props.className}` : ""
      }`}
      ref={ref}
    >
      <div className="current" onClick={() => setOpen(!isOpen)}>
        {props.children.find((ch) => ch.value === props.value)?.label}
      </div>
      <div className="menu">
        {props.children.map((option, i) => (
          <div
            key={i}
            className={`option${
              props.value === option.value ? " selected" : ""
            }`}
            onClick={() => {
              props.setValue(option.value);
              setOpen(false);
            }}
          >
            {option.label}
          </div>
        ))}
      </div>
    </div>
  );
};

export default Select;

Here is some preliminary CSS for it.


.Select {
    position: relative;
    min-width: 150px;
    user-select: none;

    .current {
        // The currently selected item
    }

    &.open {
        // Open state
    }

    .menu {
        position: absolute;
        top: calc(100% + 5px);
        max-height: 0;
        overflow-y: auto;
        overflow-x: hidden;
        width: 100%;

        .option {
            // Each option in the menu

            &.selected {
                // The currently selected option
            }
        }
    }

    &.open .menu {
        // The open menu
        max-height: 200px;
        z-index: 1;
    }
}