React UI ライブラリ『Mantine』の使い方

# この記事について

この記事では React UI ライブラリ [Mantine](https://mantine.dev/) の基本的な使い方について紹介します。
その他、[Tailwind CSS](https://tailwindcss.com/) と合わせて使う方法についても記載します。

**Mantine の特徴 :rocket:**

1. 100種類以上のカスタマイズ可能なコンポーネント
2. 50以上の便利なフック
3. TypeScript ベース
4. Next.js、Remix などのモダンフレームワークをサポート
5. ダークテーマ対応
6. スタイルを上書き可能
7. 柔軟なテーマ設定
8. メンテナ・コミュニティによって構築された[豊富なサンプル集](https://ui.mantine.dev/) (執筆時点で134種類)
9. (個人的に) デフォルトのデザインが美しく、ドキュメントが見やすい

また、スター数は直近3ヶ月で3000近く増えており、リリース速度も速く勢いがあります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/5adec0c5-31a4-c592-f213-21db48faec1b.png" width="70%">

サンプルとして簡単な TODO アプリを作ってみましたが、とても使いやすい印象でした。
https://mantine-demo-one.vercel.app/todo
(コード) https://github.com/RikuOta/mantine-demo

# 基本

## 始め方

**1. インストール**

[Getting started](https://mantine.dev/pages/getting-started/) に従ってインストールします。(1、2分)
Next.js、Remix などの各種フレームワークを使用する場合は、↑ページ内の `Framework guides` を参照。

**2. コンポーネントを探す**

[ドキュメント](https://mantine.dev/pages/getting-started/)から、以下のどちらかの方法で目的のコンポーネントを探します。

- 右上の検索フィールドから探す (`Cmd + K` もしくは `Ctrl + K` でも可)
- ナビゲーション内から探す
    - `Input` `Button` などの基本的なコンポーネントは `MANTINE CORE` 内にあります

**3. デザインを調節**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/51a10462-ba1d-d003-6d40-bc291e9c134d.png" width="70%">

より細かい調節をする場合は、Usage 以下のセクションや、`component props` タブを参照。
後は import して使うだけです。

```tsx
import { Button } from '@mantine/core';

function Demo() {
  return (
    <Button variant="outline" color="teal" size="md">
      Settings
    </Button>
  )
}
```

## テーマ設定

デフォルトのテーマ (色、余白、ブレイクポイントなど) を、デザイン要件に合うよう上書き・追加できます。
手順としては、`<App />` などのアプリケーションのルートコンポーネントを `<MantineProvider>` でラップし、`theme` プロパティに設定を渡す形になります。
詳細は [Theme object](https://mantine.dev/theming/theme-object/#colors) をご参照ください。

参考までに、独自カラーを追加する例を記載します。
※ Mantine のカラー定義については [Colors](https://mantine.dev/theming/colors/) を参照

```tsx
import { Button, MantineProvider, MantineThemeOverride } from '@mantine/core'

const theme: MantineThemeOverride = {
  // ブランドカラーを追加
  colors: {
    brand: ['#F0BBDD', '#ED9BCF', '#EC7CC3', '#ED5DB8', '#F13EAF', '#F71FA7', '#FF00A1', '#E00890', '#C50E82', '#AD1374']
  },
  // ブランドカラーをデフォルトカラーにする
  primaryColor: 'brand'
}

function Demo() {
  return (
    <MantineProvider theme={theme}>
      <Button>Primary button</Button>
    </MantineProvider>
  )
}
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/59bbf911-27f7-1d86-a320-b8a2968babf2.png" width="164px">

型補完を効かせたい場合は、以下のようにします。

```tsx
import {
  ...
  DefaultMantineColor, // 追加
  Tuple // 追加
} from '@mantine/core'

// 追加
type ExtendedCustomColors = 'brand' | DefaultMantineColor;
declare module '@mantine/core' {
  export interface MantineThemeColorsOverride {
    colors: Record<ExtendedCustomColors, Tuple<string, 10>>;
  }
}

// この下は変更なし
...
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/1b045bc8-4e54-36fd-e316-93a416ebbe8b.png" width="70%">

良い感じ :star2: 

## アイコンの使い方

Mantine にアイコン自体のコンポーネントはないため、[Tabler Icons](https://tablericons.com/) や [Radix Icons](https://icons.radix-ui.com/) など、プロジェクトに合ったものを使用してください。

Mantine のドキュメント内でサンプルとして使用されているのは [Tabler Icons](https://tablericons.com/) で、以下のように使用できます。

インストール:

```sh
yarn add @tabler/icons
```

import して使用:

```tsx
import { IconAward } from '@tabler/icons';

function Demo() {
  return (
    <IconAward
      size={36}   // set custom `width` and `height`
      color="red" // set `stroke` color
      stroke={3}  // set `stroke-width`
      strokeLinejoin="miter" // override other SVG props
    />
  )
}
```

(補足1) Mantine の色を適用する:

```tsx
import { useMantineTheme } from '@mantine/core'
import { IconUser } from '@tabler/icons'

function Demo() {
  const theme = useMantineTheme()
  return (
    <IconUser color={theme.colors.gray[6]} />
  )
}
```

(補足2) アイコン付きボタン:

```tsx
<Button
  color="red"
  size="md"
  leftIcon={<IconTrash size={18} />}
>
  Delete
</Button>
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/4351a884-7f6d-0dee-291d-023569ea6dc5.png" width="152px">

## 余白の取り方

全ての Mantine コンポーネントは、以下のような余白を取るためのプロパティを受け入れます。
※ 詳細は [shared-props](https://mantine.dev/pages/basics/#shared-props) を参照

- margin
    - `m` `my` `mx` `mt` `mb` `ml` `mr`
- padding
    - `p` `py` `px` `pt` `pb` `pl` `pr`

使用例:

```tsx
// 下部に lg 分の margin を取る
//
// (補足) lg は `theme.spacing` に定義されています
// { xs: 10, sm: 12, md: 16, lg: 20, xl: 24 }
<Button mb="lg">
  Hello
</Button>
```

注意点として、当然ですが `<p>` などの通常の HTML 要素に指定することはできません。
少し冗長ですが、[Box](https://mantine.dev/core/box/) や [Space](https://mantine.dev/core/space/) を使って書く必要があります。

```tsx
// 下部に lg 分の margin を取る
<Box component="p" mb="lg">
  Hello
</Box>

<p></p>
<Space h="lg" /> // ここに lg 分の高さを取る
<p></p>
```

(備考)
余白はクラスで指定した方が断然楽なので [Tailwind CSS](https://tailwindcss.com/) などの他ライブラリに任せるのが良い気がします。

```tsx
// HTML 要素、Mantine コンポーネント両方で同じように使える
<p className="mb-4">...</p>
<Button className="mb-4">...</Button>
```

## Hooks

以下のような便利なフックが50近く提供されています。
[ドキュメント](https://mantine.dev/pages/getting-started/)のナビゲーション内 `MANTINE HOOKS` に一覧があります。

- 要素サイズ取得 ([use-element-size](https://mantine.dev/hooks/use-element-size/))
- debounce 機能提供 ([use-debounced-value](https://mantine.dev/hooks/use-debounced-value/))
- 指定時間、ページ内で何もしなかったかどうか検出 ([use-idle](https://mantine.dev/hooks/use-idle/))

# レスポンシブ対応 (メディアクエリ)

メディアクエリはいくつか方法が提供されています。
詳細は [Responsive styles](https://mantine.dev/styles/responsive/) をご参照ください。

[MediaQuery コンポーネント](https://mantine.dev/core/media-query/)を使う例:

```tsx
<MediaQuery smallerThan="lg" styles={{ display: 'none' }}>
  <span>画面幅が lg 以上なら表示される</span>
</MediaQuery>
```

(備考)
Mantine のメディアクエリは使用せず、[Tailwind CSS](https://tailwindcss.com/) などの他ライブラリに任せた方が簡潔に書ける気がします。

```tsx
<p className="hidden lg:block">
  画面幅が lg 以上なら表示される
</p>
```

# コンポーネントのスタイルを上書きする

各コンポーネントは、クラス or インラインスタイルによる内部の要素全てのスタイル上書きをサポートしています。
カスタマイズオプションと合わせることで、様々なデザイン要件に適合させることができます。
詳細は [Styles API](https://mantine.dev/styles/styles-api/) をご参照ください。

参考までに `Button` のスタイルを上書きする例を記載します。
これを、

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/0b9652fd-ee08-2632-0671-6a396033b3d9.png" width="207px">

こうしたい :arrow_down:

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/0f994183-7ac5-61cb-e780-6a1f0bc777a2.gif" width="220px">

まず、[Button コンポーネント](https://mantine.dev/core/button/)の `Styles API` タブを確認し、`Button` がどのようなパーツで構成されているかを確認します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/e78e9df8-2774-e46a-6af1-f50aa30ce7ee.png" width="70%">

上記テーブルの `Name` 列を元に、スタイルを上書きします。
今回は `inner` と `root` を上書きしています。

```tsx
<Button
  size="md"
  variant="subtle"
  leftIcon={<IconShoppingCart size={18} />}
  
  // スタイルを上書きする
  //
  // (補足)
  // `styles={theme => ({ ... })}` のように関数にすることで
  // テーマオブジェクト (https://mantine.dev/theming/theme-object) にアクセス可能
  styles={{
    inner: {
      position: 'relative',
      zIndex: 1
    },
    root: {
      position: 'relative',
      border: '2px solid',
      borderRadius: '100px',
      // 少し右下にズレた背景を作成
      '&::before': {
        content: '""',
        position: 'absolute',
        top: 0,
        left: 0,
        zIndex: -1,
        width: '100%',
        height: '100%',
        borderRadius: '100px',
        backgroundColor: 'currentColor',
        opacity: 0.15,
        transform: 'translate(5px, 5px)',
        transition: '0.3s'
      },
      // 既存のホバーアニメーションを削除
      '&:hover': {
        background: 'none'
      },
      // ホバーした際、ズレている背景色を正しい位置に戻す
      '&:hover::before': {
        transform: 'translate(0, 0)'
      }
    }
  }}
>
  カートに入れる
</Button>
```

例えば [Vuetify](https://vuetifyjs.com/) (Vue の UI ライブラリ) だと、SASS 変数や props でどうにもならないとき、

1. 開発者ツールを開く
2. クラス名を特定
3. Deep Selector で上書き

のような手間 & 保守性の低い方法 (アップデートでクラス名が変更されたら崩れる) で上書きしていたため、公式で上書き方法のサポートがあるのは助かっています。

**(補足1) IDE の補完**

コンポーネントのパーツ構成は IDE の補完でも確認できるため、毎回ドキュメントを見る必要はありません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/488228/2b8a4d8d-1dab-f950-2a4f-13b8c43e762a.png" width="70%">

**(補足2) クラスで上書き**

クラスでも上書きできるため、[Tailwind CSS](https://tailwindcss.com/) のような Utility-First なライブラリとは相性が良いです。

```tsx
<Button classNames={{
  root: 'shadow-md',
  label: 'font-mono tracking-wider'
}}>
  Hello
</Button>
```

**(補足3) sx プロパティ**

`root` のみを上書きしたい場合は [sx プロパティ](https://mantine.dev/styles/sx/)を使うと簡潔に書けます。

```tsx
<Text sx={{ color: '#000', fontSize: 18 }}>
  Hello
</Text>
```

**(補足4) スタイルを全て削除する**

既存のスタイルを全て削除して一から定義したい場合は [unstyled プロパティ](https://mantine.dev/styles/styles-api/#unstyled-components)を使うことができます。

```tsx
<Button unstyled>
  Hello
</Button>
```

# Tailwind CSS と合わせて使う

Mantine を単体で使ってみた結果、以下のような点が不都合に感じました。

- 余白を取るのが面倒
    - `m` や `pb` などの余白プロパティは通常の HTML 要素に指定できない
- コンポーネントの調節が props だけで対応できないとき、もしくは Mantine に存在しないコンポーネントを一から作るとき、レスポンシブなスタイルを一から書くのが面倒

UI 自体は Mantine に任せ、余白・レイアウト・スタイリングなどの調節を [Tailwind CSS](https://tailwindcss.com/) に任せると解決できそうでした。

ただ、以下の設定を Mantine と Tailwind の間で合わせる必要がありましたので、試してみた結果を記載します。

1. ベーススタイル
2. ブレイクポイント
3. 余白 (margin、padding)
4. 色

## 1. ベーススタイル

- Mantine
    - [normalize.css](https://necolas.github.io/normalize.css/) + 独自のスタイル
    - 参考: [css-reset-and-global-styles](https://mantine.dev/theming/mantine-provider/#css-reset-and-global-styles)
- Tailwind
    - [modern-normalize](https://github.com/sindresorhus/modern-normalize) をベースに構築された独自のスタイル
    - 参考: [Preflight](https://tailwindcss.com/docs/preflight#border-styles-are-reset-globally)

両方使用すると設定がぶつかるため、片方を無効にします。
UI 構築を任せる Mantine の定義を使用した方が影響は少ないかと思います。

Tailwind の Preflight を無効にする:

```tailwind.config.js
module.exports = {
  corePlugins: {
    preflight: false,
  }
}
```

## 2. ブレイクポイント

- Mantine のデフォルトのブレイクポイント
  - xs: `576px`
  - sm: `768px`
  - md: `992px`
  - lg: `1200px`
  - xl: `1400px`
  - 参考: [Responsive styles](https://mantine.dev/styles/responsive/)
- Tailwind のデフォルトのブレイクポイント
  - sm: `640px`
  - md: `768px`
  - lg: `1024px`
  - xl: `1280px`
  - 2xl: `1536px`
  - 参考: [Responsive Design](https://tailwindcss.com/docs/responsive-design#using-custom-breakpoints)

ブレイクポイントは、メディアクエリ全般を任せる Tailwind の定義を使用しました。

Tailwind のブレイクポイントで Mantine のブレイクポイントを上書きする:

```app.tsx
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
import resolveConfig from 'tailwindcss/resolveConfig'
import tailwindConfig from '../tailwind.config.js'

const resolvedTailwindConfig = resolveConfig(tailwindConfig)

// Tailwind のブレイクポイントを Mantine 用に変換
const tailwindScreensForMantine: {[key: string]: number} = {}
const tailwindScreens = resolvedTailwindConfig.theme?.screens
if (tailwindScreens) {
  for (const [key, value] of Object.entries(tailwindScreens)) {
    // 末尾の px を削除してから数値に変換
    tailwindScreensForMantine[key] = Number(value.slice(0, -2))
  }
}

const theme: MantineThemeOverride = {
  breakpoints: tailwindScreensForMantine
}

function Demo() {
  return (
    <MantineProvider theme={theme}>
      ...
    </MantineProvider>
  )
}
```

## 3. 余白

- Mantine のデフォルトの余白
    - xs: `10px`
    - sm: `12px`
    - md: `16px`
    - lg: `20px`
    - xl: `24px`
    - 参考: [Spacing and padding](https://mantine.dev/pages/basics/#spacing-and-padding)
- Tailwind のデフォルトの余白
    - 0: `0px`
    - px: `1px`
    - 0.5: `0.125rem`
    - 1: `0.25rem`
    - ...沢山...
    - 96: `24rem`
    - 参考: [default-spacing-scale](https://tailwindcss.com/docs/customizing-spacing#default-spacing-scale)

余白は Tailwind の定義を使おうと思ったのですが、設定を合わせたところ以下のようになってしまい、使用方法で混乱しそうでした。

```tsx
// 単純な 10px
<Button mb={10} />

// Tailwind の 10 (2.5rem)
<Button mb="10" />
```

『余白の調節は Tailwind のクラスでのみ行う』のような実装方針でカバーするのが良さそうでした。

```tsx
// Tailwind の 10 (2.5rem)
<Button className="mb-10" />
```

## 4. 色

- Mantine のデフォルトの色
    - [open-color](https://yeun.github.io/open-color/) に若干の追加を行ったカラーパレット
    - 参考: [Default colors](https://mantine.dev/theming/colors/#default-colors)
- Tailwind のデフォルトの色
    - 独自のカラーパレット
    - 参考: [Customizing Colors](https://tailwindcss.com/docs/customizing-colors)

色は Tailwind の定義を使ったところ、以下のような使用感になり、微妙な印象でした。

```tsx
<Badge color="gray.7" className="text-gray-700">
  Hello
</Badge>
```

なので、以下のようにして Mantine の定義を使用することにしました。
(公式の方法ではない点のみ注意)

```tailwind.config.js
// Mantine の色を Tailwind 用に変換
//
// 公式の方法がないため、以下のカラーファイルを使用
// https://github.com/mantinedev/mantine/blob/master/src/mantine-styles/src/theme/default-colors.ts
const { DEFAULT_THEME } = require('@mantine/core')
const mantineColorsForTailwind = {}
for (const [name, colors] of Object.entries(DEFAULT_THEME.colors)) {
  mantineColorsForTailwind[name] = colors.reduce((acc, cur, i) => {
    return { ...acc, [i]: cur }
  }, {})
}

module.exports = {
  theme: {
    colors: mantineColorsForTailwind
  }
}
```

以下のように使用:

```tsx
<p className="text-dark-0">Hello</p>
<p className="bg-grape-9">World</p>
```

以上です！
ぜひ使ってみてください〜。