Components / Inputs & Forms

Select

Dropdown menus for choosing from a list of options.

Default

With preselected value

Disabled

Error state

Please select a service type.

Source SelectDemo.tsx

The exact code behind the live demo above. Fetch it raw at /components/source/select.txt.

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

function Section({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <div className="mb-8">
      <h3 className="text-sm font-bold text-[var(--flux-heading)] mb-3">{title}</h3>
      <div className="p-6 rounded border border-[var(--flux-grey-100)] bg-[var(--flux-surface)]">
        {children}
      </div>
    </div>
  );
}

const labelClass = "block text-sm font-medium text-[var(--flux-black)] mb-1";

interface CustomSelectProps {
  label: string;
  options: string[];
  placeholder?: string;
  disabled?: boolean;
  error?: string;
  defaultValue?: string;
}

function CustomSelect({
  label,
  options,
  placeholder = "Select an option...",
  disabled = false,
  error,
  defaultValue,
}: CustomSelectProps) {
  const [open, setOpen] = useState(false);
  const [selected, setSelected] = useState(defaultValue || "");
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    function handleClick(e: MouseEvent) {
      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
    }
    document.addEventListener("mousedown", handleClick);
    return () => document.removeEventListener("mousedown", handleClick);
  }, []);

  const borderColor = error
    ? "border-[var(--flux-error)]"
    : open
      ? "border-[var(--flux-primary-400)]"
      : "border-[var(--flux-grey-200)]";

  const ringClass = open
    ? error
      ? "ring-1 ring-[var(--flux-error)]"
      : "ring-1 ring-[var(--flux-primary-300)]"
    : "";

  return (
    <div className="max-w-sm" ref={ref}>
      <label className={labelClass}>{label}</label>
      <div className="relative">
        <button
          type="button"
          disabled={disabled}
          onClick={() => !disabled && setOpen(!open)}
          className={`w-full flex items-center justify-between px-3 py-2 rounded border ${borderColor} ${ringClass} bg-[var(--flux-surface)] text-sm text-left outline-none transition-all ${
            disabled ? "opacity-50 cursor-not-allowed bg-[var(--flux-grey-50)]" : "cursor-pointer hover:border-[var(--flux-grey-300)]"
          }`}
        >
          <span className={selected ? "text-[var(--flux-black)]" : "text-[var(--flux-grey-300)]"}>
            {selected || placeholder}
          </span>
          <svg
            className={`w-4 h-4 text-[var(--flux-grey-500)] transition-transform ${open ? "rotate-180" : ""}`}
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
          </svg>
        </button>

        {open && (
          <ul className="absolute z-50 mt-1 w-full rounded border border-[var(--flux-grey-200)] bg-[var(--flux-surface)] shadow-[var(--flux-shadow-lg)] py-1 max-h-60 overflow-auto">
            {options.map((opt) => (
              <li key={opt}>
                <button
                  type="button"
                  onClick={() => {
                    setSelected(opt);
                    setOpen(false);
                  }}
                  className={`w-full text-left px-3 py-2 text-sm transition-colors cursor-pointer ${
                    selected === opt
                      ? "bg-[var(--flux-primary-50)] text-[var(--flux-primary-800)] font-medium"
                      : "text-[var(--flux-black)] hover:bg-[var(--flux-grey-50)]"
                  }`}
                >
                  <span className="flex items-center justify-between">
                    {opt}
                    {selected === opt && (
                      <svg className="w-4 h-4 text-[var(--flux-primary-500)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
                      </svg>
                    )}
                  </span>
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
      {error && <p className="mt-1 text-xs text-[var(--flux-error)]">{error}</p>}
    </div>
  );
}

const serviceTypes = ["Departure", "Accommodation", "Activity", "Transfer"];

export default function SelectDemo() {
  return (
    <div>
      <Section title="Default">
        <CustomSelect label="Service type" options={serviceTypes} />
      </Section>

      <Section title="With preselected value">
        <CustomSelect label="Service type" options={serviceTypes} defaultValue="Accommodation" />
      </Section>

      <Section title="Disabled">
        <CustomSelect label="Service type" options={serviceTypes} disabled />
      </Section>

      <Section title="Error state">
        <CustomSelect
          label="Service type"
          options={serviceTypes}
          error="Please select a service type."
        />
      </Section>
    </div>
  );
}