【React】tailwindcssで作るMantineUIのMultiSelectコンポーネント

はじめに

この記事の概要

こんにちは、株式会社TOKOSのスギタです!
今回はtailwindcssを用いてUIライブラリMantineを使いMultiSelectコンポーネントを作成する方法を説明していきたいと思います。

対象読者

  • Reactを用いたWEB制作、WEB開発を行っている方
  • tailwindcssを使用しながらUIライブラリを使用したい方

MantineUIについて

UIライブラリといえば思いつくのが、Material-UI、BootStrap、AntDesign、Chakra UI辺りだと思います。
UIライブラリを用いることで、スピーディーなフロントエンド開発、インタラクティブなUIを実現させることが容易になります。
多くのフロントエンド開発何かしらのUIライブラリを用いているのでは無いでしょうか?

UIライブラリのメリットは先程挙げた事になりますが、デメリットももちろんあります。
思いつくデメリットとしては、スタイルの変更が容易では無いことやUIライブラリの制約が強いことではないでしょうか?
場合によっては、tailwindcssと親和性が低いというところもあると思います。

だったらヘッドレスUIを使えばとなると思いますが、ヘッドレスUIの代表的ライブラリのHeadlessUIやRadixではUIの種類が少ないというのもあるのでは無いでしょうか。

そこで紹介したいのがMantineです。

MantineUIについて

皆さんMantineは知っていますか?
聞いたこともない人も多いと思うので軽く説明したいと思います!

MantineはReact専用のUIライブラリになります。

私が思うMantineの強みを5つあげたいと思います。

  • 100種類以上のコンポーネントや50種類以上の便利のHooks
  • Next.jsはもちろんモダンなフレームワークをサポート
  • TypeScriptベース
  • tailwindcssを用いてスタイルの上書き可能、ヘッドレスUIのような使用方法が可能
  • 公式ドキュメントの可読性の良さ

上記5つの中でもスタイルの上書きできることが、かなり良いなと感じます。

開発環境

開発環境は下記になります。

  • React 18.0.28
  • Next.js13.2.3
  • TypeScript 4.9.5
  • Mantaine 6.0.13

下記参照内でフレームワークを選択してください。
今回はNext.jsで行っていきたいと思います!
また使用したいパッケージを選択してインストールしてください。
今回はmantine/core内にあるコンポーネントを使用するので、@mantine/coreをインストールしています。

tailwindcssとの整合性をあわせる

問題点

tailwindcss×Mantineの場合問題なことが2点あります。

  • ブレークポイントの整合性
  • リセットCSSの整合性

この上記2つを解消しないとスタイルの崩れや、意図しない挙動になってしまいます。

ブレークポイントの統一

前提としてtailwindとMantineでお互いにブレークポインを持ってます。

下記はtailwindのブレークポイント(初期値)です。

  • sm : 640px
  • md : 768px
  • lg : 1024px
  • xl : 1280px
  • 2xl : 1536px

次はMantaineのブレークポイントです。(公式では1em=16pxでem表記になってます)

  • xs : 576px
  • sm : 768px
  • md : 992px
  • lg : 1200px
  • xl : 1400px

上記でも分かる通り、名称と値がそれぞれ違います。これを統一したほうが、意図しない挙動を避けるためには必要です。
特に問題がなければMantaine側に合わせるのがbetterだと思います。

module.exports = {
  theme: {
    extend: {
      // Mantineのbreak-pointに合わせています
      screens: {
        lg: "1200px",
        md: "992px",
        sm: "768px",
        xl: "1400px",
        xs: "576px",
      },
    },
  },
}

これでブレークポイントの問題は解消です。

リセットCSSの統合

次にリセットCSSの競合解消です。
様々解消の方法はあると思いますが、個人的には、Mantine側のリセットCSSを削除する方法が一番良いかなと感じております。
Mantaine側のリセットCSSを採用したところ、破壊的なスタイルが多かったためです。
逆にtailwind側のリセットCSSを採用した場合わかる範囲で問題となった箇所はButton関係のコンポーネントを使用する際に背景が消える点です。
リセットCSSで背景が消えてもCSSでスタイルを上書きができるので、問題がないと思っています!
またMantineのButtonコンポーネントを使うよりボタンに関しては自作コンポーネントを使うほうが多いかなと思っています!

影響範囲について

すべてのコンポーネントに対して試したわけでは無いので、ご了承ください。他に良い方法があればご教授していただけると幸いです🙇‍♂️

ではMantineのリセットCSSの削除方法ですが、pages/_app.tsxのMantineProvider内の記述を削除すれば良いです。

import { AppProps } from 'next/app';
import Head from 'next/head';
import { MantineProvider } from '@mantine/core';

export default function App(props: AppProps) {
  const { Component, pageProps } = props;

  return (
    <>
      <Head>
        <title>Page title</title>
        <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
      </Head>

      <MantineProvider
        withGlobalStyles
        withNormalizeCSS
        theme={{
          /** Put your mantine theme override here */
          colorScheme: 'light',
        }}
      >
        <Component {...pageProps} />
      </MantineProvider>
    </>
  );
}

ハイライトされている2行を削除します。
今回はGrobalStylesも不要なので削除しています。

これで準備完了です。

コンポーネントの作成例

今回は例としてMantineのCoreパッケージの中からMultiSelectを使ってみたいと思います。

完成予定

今回の完成予定は下記になります。

MultiSelectの実装

まずは公式ドキュメンと内のサイドバーのMANTINE COREのinputs内のMultiSelectを押してください。

ドキュメント内から要件によって近しいものを選択肢コピペすればOKになります。
今回は機能的要件はあまり求めていないので一番上にある「A bare minimum example」の必要な箇所をコピペします。

import { MultiSelect } from '@mantine/core';

const data = [
  { value: 'react', label: 'React' },
  { value: 'ng', label: 'Angular' },
  { value: 'svelte', label: 'Svelte' },
  { value: 'vue', label: 'Vue' },
  { value: 'riot', label: 'Riot' },
  { value: 'next', label: 'Next.js' },
  { value: 'blitz', label: 'Blitz.js' },
];

const ExamplePage: NextPageWithLayout = () => {
  return (
    <MultiSelect
      data={data}
      label="Your favorite frameworks/libraries"
      placeholder="Pick all that you like"
    />
  );
}

ここから今回の完成に向け必要な箇所、不必要な箇所を修正します。

import { MultiSelect } from '@mantine/core';

const data = [
  { label: "ユーザー1", value: "userId1" },
  { label: "ユーザー2", value: "userId2" },
  { label: "ユーザー3", value: "userId3" },
  { label: "ユーザー4", value: "userId4" },
  { label: "ユーザー5", value: "userId5" },
  { label: "ユーザー6", value: "userId6" },
  { label: "ユーザー7", value: "userId7" },
];

const ExamplePage: NextPageWithLayout = () => {
  return (
    <MultiSelect
      data={data}
      placeholder="ユーザーを選択してください"
    />
  );
}

これでドキュメントのように表示できていると思います。
現状と完成予定と比べると下記と相違があります。

  • 全体のスタイリング+(左のユーザーアイコン)
  • 閉じる開くに連動する矢印アイコン
  • 選択肢がなくなった場合の文字の「選択肢がありません」の表示

はじめにスタイルの調整から行います。


コンポーネントに対しPropsでsize等のPropsを渡すことも可能ですが、細かく調整したい場合はコンポーネント内の要素に個別プロパティを当てることも可能です。その場合はドキュメント内タブメニューの「Styles API」で確認します。

ドキュメントをに書いてある通り、関わってくる箇所(Name)としては「root」「input」「value」「values」辺りでしょうか!
かなりドキュメントも読みやすいのではないでしょうか!

確認後tailwindでスタイリングしていきます。”classNames”で複数形なので注意してください!

import { MultiSelect } from '@mantine/core';

const data = [
  { label: "ユーザー1", value: "userId1" },
  { label: "ユーザー2", value: "userId2" },
  { label: "ユーザー3", value: "userId3" },
  { label: "ユーザー4", value: "userId4" },
  { label: "ユーザー5", value: "userId5" },
  { label: "ユーザー6", value: "userId6" },
  { label: "ユーザー7", value: "userId7" },
];

const ExamplePage: NextPageWithLayout = () => {
  return (
   <div className="max-w-[480px] flex border border-gray-200 shadow rounded-full py-3 px-6">
     //ユーザーアイコンの追加
     <OutLinePersonIcon width={32} height={32} className="fill-blue-500" />
      <MultiSelect
        data={data}
        placeholder="ユーザーを選択してください"
        //下記でスタイリング、classNamesなので注意
        classNames={{
         input: "border-0",
         root: "w-full",
         value: "h-8 bg-tint-blue-200",
         values: "overflow-y-scroll h-8",
        }}
      />
    </div>
  );
}

上記のように要素に対し個別でスタイルを与えることができます。
どこがどこだかわからない場合はDevツールで確認するのも良いと思います!

必要な機能はドキュメント内タブメニューの「Component props」で確認します。

閉じる開くに連動する矢印アイコンは「rightSection」でアイコンを追加し、選択欄が開くとき発火するコールバック関数「onDropdownOpen」、選択欄が閉じるとき発火するコールバック関数「onDropdownClose」を使って矢印の制御をしていきます。

選択欄が開いている状態閉じる状態をbooleanで状態管理したいので、useStateで定義していきます。
三項演算子を用いてアイコンが回転するスタイルを追記していきます。

import { MultiSelect } from '@mantine/core';

const data = [
  { label: "ユーザー1", value: "userId1" },
  { label: "ユーザー2", value: "userId2" },
  { label: "ユーザー3", value: "userId3" },
  { label: "ユーザー4", value: "userId4" },
  { label: "ユーザー5", value: "userId5" },
  { label: "ユーザー6", value: "userId6" },
  { label: "ユーザー7", value: "userId7" },
];

const ExamplePage: NextPageWithLayout = () => {
  const [isOpen, setIsOpen] = useState(false)
  return (
   <div className="max-w-[480px] flex border border-gray-200 shadow rounded-full py-3 px-6">
     //ユーザーアイコンの追加
     <OutLinePersonIcon width={32} height={32} className="fill-blue-500" />
      <MultiSelect
        data={data}
        placeholder="ユーザーを選択してください"
        //下記でスタイリング、classNamesなので注意
        classNames={{
         input: "border-0",
         root: "w-full",
         value: "h-8 bg-tint-blue-200",
         values: "overflow-y-scroll h-8",
         //アイコンの記述
         rightSection: `pointer-events-none fill-gray-500 ${
           isOpen
           ? "rotate-180 transition duration-200"
           : "rotate-0 transition duration-200"
         }`,
        }}
        rightSection={
         <ChevronBottomIcon width={24} height={24} fill="#636B7E" />
     }       
        onDropdownOpen={() => setIsOpen((prev) => !prev)}
        onDropdownClose={() => setIsOpen((prev) => !prev)}
        //選択肢が無いときの記述
        nothingFound="選択肢がありません"
      />
    </div>
  );
}

これで完成になります。
他にも必要なpropsを選択肢自分でカスタムしてみてください!

さいごに


今回はReact専用のUIライブラリのMantineを紹介しました。
他のライブラリと比べかなり自由度が高いと感じました!
またtailwindを使ってスタイリングできるのは他に無いところではないでしょうか!
今回はMultiSelectを使用しましたが、他にも便利なコンポーネントやHooksが用意されているので、みなさんもぜひ使ってみてください!