react-selectの見た目をTailwindCSSで整える
2023/08/27

react-selectの見た目をTailwindCSSで整える

5 mins to read

RadixUIとかheadlessUIとかいろいろありますが、selectboxに関してはreact-selectが一番使い勝手がいい気がします。特にMultiple SelectとかCreatable Selectを作りたい時はほぼ一択ですよね。

ただやっぱりCSSのカスタマイズ方法がちょっとややこしいので、TailwindCSSで見た目を整える方法をここにまとめておきます。

Selectコンポーネント

早速ですが、完成したSelectコンポーネントはこちらです。

components/ui/select.tsx
import SelectComponent, {
  components,
  DropdownIndicatorProps,
  MultiValueRemoveProps,
  ClearIndicatorProps
} from "react-select";
import Creatable from "react-select/creatable";
import { ChevronDown, X } from "lucide-react";
import { clsx } from "clsx";

const DropdownIndicator = (props: DropdownIndicatorProps) => {
  return (
    <components.DropdownIndicator {...props}>
      <ChevronDown size={18} strokeWidth={1.4} />
    </components.DropdownIndicator>
  );
};

const ClearIndicator = (props: ClearIndicatorProps) => {
  return (
    <components.ClearIndicator {...props}>
      <X size={18} strokeWidth={1.4} />
    </components.ClearIndicator>
  );
};

const MultiValueRemove = (props: MultiValueRemoveProps) => {
  return (
    <components.MultiValueRemove {...props}>
      <X size={12} strokeWidth={3} />
    </components.MultiValueRemove>
  );
};

const selectStyles = {
  control: {
    base: "flex border rounded-md bg-input hover:cursor-pointer min-w-[100px] !min-h-[40px] w-[100%] h-[100%] text-foreground text-foreground",
    focus: "border border-primary shadow-[0_0_0_2px_rgba(var(--primary),0.3)]",
    nonFocus: "border-border"
  },
  option: {
    base: "hover:cursor-pointer px-3 py-2 rounded flex gap-2 items-center hover:bg-muted !text-sm",
    focus: "",
    selected: "bg-[rgba(var(--primary),0.15)] hover:bg-[rgba(var(--primary),0.15)] text-primary font-bold"
  }
};

const classNames = {
  control: ({ isFocused }: any) =>
    clsx(
      isFocused ? selectStyles.control.focus : selectStyles.control.nonFocus,
      selectStyles.control.base
    ),
  option: ({ isSelected, isFocused }: any) =>
    clsx(
      selectStyles.option.base,
      isSelected ? selectStyles.option.selected : "",
      isFocused ? selectStyles.option.focus : ""
    ),
  menu: () => "p-1 mt-1 bg-card rounded-md shadow-md",
  placeholder: () => "text-placeholder pl-1",
  input: () => "text-foreground pl-1",
  valueContainer: () => "p-1 gap-1",
  singleValue: () => "pl-1",
  multiValue: () => "flex bg-[rgba(var(--primary),0.15)] p-0 pl-2 rounded-sm text-xs overflow-hidden",
  multiValueLabel: () => "py-1",
  multiValueRemove: () => "bg-[rgba(var(--primary),0.1)] hover:bg-[rgba(var(--primary),0.2)] text-primary px-1 ml-2",
  indicatorsContainer: () => "p-1 gap-1",
  clearIndicator: () => "text-muted-foreground px-1 hover:text-error",
  indicatorSeparator: () => "bg-border",
  dropdownIndicator: () => "p-1 hover:bg-muted text-muted-foreground rounded-md hover:text-foreground",
  groupHeading: () => "ml-3 mt-2 mb-1 text-muted-foreground text-sm",
  noOptionsMessage: () => "text-placeholder py-3"
};

type Option = { [string: string]: any }

type SelectProps = {
  options: Array<Option>;
  className?: string;
  clearValue?: () => void;
  creatable?: boolean;
  isMulti?: boolean;
  value?: any;
  onChange?: () => void;
};

export const Select = ({
  options,
  className,
  creatable = false,
  isMulti = false,
  value,
  onChange
}: SelectProps) => {
  if (creatable) {
    return (
      <Creatable
        unstyled
        options={options}
        isMulti={isMulti}
        value={value}
        onChange={onChange}
        classNames={{ ...classNames }}
        className={clsx(className)}
        components={{ DropdownIndicator, ClearIndicator, MultiValueRemove }}
      />
    );
  }
  return (
    <SelectComponent
      unstyled
      options={options}
      isMulti={isMulti}
      value={value}
      onChange={onChange}
      classNames={{ ...classNames }}
      className={clsx(className)}
      components={{ DropdownIndicator, ClearIndicator, MultiValueRemove }}
    />
  );
};

unstyledを指定してデフォルトのスタイルを消してから、classNamesでそれぞれのパーツにTailwindのクラス名を当てています。

私はTailwindCSSを使う時、globals.csstailwind.config.jsから使える色の種類を増やしてしているので(primary, foreground, placeholderなど)このままだと動きませんが、それぞれ自分が使いたい色に書き換えると動くと思います。

clsxを使ってクラス名の動的な付け替えを行なっていますが、こちらの記事にあるcn関数を使っても構いません。

componentsから各種アイコンもカスタマイズしています。

使い方

作成したセレクトコンポーネントを使いたいときはこういう感じで使います。

const options = [
  {
    label: "Mammal",
    options: [
      { value: "Dolphin", label: "🐬 Dolphin" },
      { value: "Giraffe", label: "🦒 Giraffe" }
    ]
  },
  {
    label: "Carnivore",
    options: [
      { value: "Tiger", label: "🐅 Tiger" },
      { value: "Lion", label: "🦁 Lion" }
    ]
  },
];

const options2 = [
  { value: "Butterfly", label: "🦋 Butterfly" },
  { value: "Fox", label: "🦊 Fox" },
  { value: "Cat", label: "🐱 Cat" }
];

<Select
  options={options}
  className="sm:w-[300px] w-[100%]"
/>

<Select
  options={options2}
  className="w-[300px]"
  creatable
  isMulti
/>

セレクトボックスのサイズはclassNameに設定すると適用されます。

creatableisMultiも設定可能です🙆‍♀️