旧ブログ(ISSEN)から移行しました

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

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

杉田侑祐
杉田侑祐10分で読めます

はじめに

この記事の概要

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

Mantine

mantine.dev

対象読者

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

MantineUIについて

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

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

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

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

Mantineの強み

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

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

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

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

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

開発環境

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

  • react: 18.0.28
  • next.js: 13.2.3
  • typescript: 4.9.5
  • @mantine/core: 6.0.13

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

https://mantine.dev/pages/getting-started/

mantine.dev

Tailwind CSSとの整合性をあわせる

問題点

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

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

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

ブレークポイントの統一

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

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

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

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

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

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

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

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

リセットCSSの統合

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

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

pages/_app.tsx
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行を削除します。
今回はGlobalStylesも不要なので削除しています。

これで準備完了です。

コンポーネントの作成例

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

完成予定

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

MultiSelectの実装

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

MultiSelect | Mantine

Custom searchable multi select

mantine.dev

ドキュメント内から要件によって近しいものを選択しコピペすれば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" />
}

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

ExamplePage.tsx
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="ユーザーを選択してください" />
}

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

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

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

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

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

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

ExamplePage.tsx
import { MultiSelect } from "@mantine/core"
import { OutLinePersonIcon } from "@/components/icons" // 自作アイコンコンポーネント
 
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で定義していきます。
三項演算子を用いてアイコンが回転するスタイルを追記していきます。

なお、OutLinePersonIconChevronBottomIconは自作のアイコンコンポーネントです。
react-icons@heroicons/reactなどのアイコンライブラリで代用可能です。

ExamplePage.tsx
import { MultiSelect } from "@mantine/core"
import { useState } from "react"
import { OutLinePersonIcon, ChevronBottomIcon } from "@/components/icons" // 自作アイコンコンポーネント
 
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 CSSを使ってスタイリングできるのは他に無いところではないでしょうか!
今回はMultiSelectを使用しましたが、他にも便利なコンポーネントやHooksが用意されているので、みなさんもぜひ使ってみてください!

この記事を書いた人

杉田侑祐
杉田侑祐

TOKOSのフロントエンドエンジニア兼UI/UXデザイナー。このブログではフロントエンドメインで投稿しています。HIPHOPとゲームが好きです✌️