Next.jsでTailwindを使ったデザインシステムの構築をしよう!
今回の記事は、以下の動画の備忘録的なものです。英語ですけど、自動翻訳の字幕でも十分理解できますし13分なのですぐ見れますので、ぜひどうぞ。
また、以下の動画紹介されているcva
(Class Variance Authority)に関する記事がQiitaでもZennでもほぼ見かけなかったので、誰かの目に届けばなと思い書いています。
これは、 Next.js Conf 2022
の動画で、我々がいつもお世話になっているDiscord
のソフトウェアエンジニアの方がTailwind CSSを使ったデザインシステムの構築について話してくれています。
今回紹介するツール
- Tailwind CSS
- Clsx
- Class-Variance-Authority
- Radix UI
なぜ、Tailwind CSSを使うのか?
MUI、Chakra UI、Mantine などのあらかじめコンポーネントが準備されているUIライブラリを使用していて以下のように思ったことはありませんか?
- このボタンなんか違うんだよなぁ・・・
- なんか設定いじりすぎて、原型留めてなくね?
- ちょっとデザイン変えたいだけなのに上手くいかない・・・
もちろん、UIライブラリが用意してくれている APIやプラグインやカスタマイズ方法などを使って実現できる部分もあるとは思いますが、与えられているデザインシステムからズレたものは実現するのが難しいです。それは、我々がデザインシステムを丸ごと借りていて、オーナーシップを失っている状態だからです。
そのため、デザインシステムの範疇を越える調整を行いたい場合は衝突が起こってしまい、仕方なくglobalのCSSにちまちま上書きしたりするんですよね。(富岡義勇が聞いたら、「デザインシステムの生殺与奪の権利を渡すな!」などと言われそうです笑)
では、どうすれば良いのか?
デザインシステムを一から作るのです。
しかし、「MUIみたいなUIライブラリ一から作れ!」という訳ではなく、プロジェクトに必要なコンポーネントだけを作れば良いんです。よく使われるコンポーネントはかなり限定的なはずです。
デザインシステムの構築に便利なのが、そうTailwind CSS
です。
Tailwind CSSの優れている点は、便利なプリセットが用意されていることです。また、これらはあくまでもプリセットであるため、もちろんプロジェクトごとに独自の変更を加えることも可能です。
なので、時々Tailwind CSSでは細かい変更ができないとか主張する人がいますが、設定を変更したり、最近ではarbitrary values
が利用できますので、細かい微調整も余裕です。
ちなみに、arbitrary values
は以下のようなものです。
<div class="top-[117px]">
<!-- ... -->
</div>
コンポーネント設計におけるCSS
というわけで、Tailwind CSSの魅力がある程度わかったところでコンポーネントにおけるCSSの設定について考えてみましょう。
コンポーネント設計では、コンポーネントが受け取ったpropsに応じて条件分岐でCSSを変えるなんてことはよく行われますよね。そんな時に、よく利用されているのがclassnames
やclsx
です。
これらは、条件付きのCSSを上手く結合してくれるライブラリです。
具体的には、こんな感じです。
import {clsx} from 'clsx'
const Button = ({intent}) => {
return(
<button className={clsx('w-4 h-2 text-white', {
'bg-blue-400' : intent === 'primary',
'bg-red-400' : intent === 'danger'
})}>
Button
</button>
)
}
export default Button
Buttonコンポーネントはintent
というpropsを受け取りその値によってCSSが変わります。
しかし、propsと条件分岐が10個も20個も追加されてしまうと、かなりのカオスな状態になってしまいます。
そんな人におすすめしたいのがClass-Variance-Authority
です。
Class-Variance-Authority(cva)とは?
簡単にいうと、variants
を定義して条件分岐をわかりやすくしようというものです。
Figmaとか使ったことある人はVariants
に馴染みがあると思います。
Variants
とは、コンポーネントにおける状態の概念と捉えるとわかりやすいと思います。上記では、intent
という状態の概念にprimary
とdanger
という二つのプロパティがあり、それぞれにbg-blue-400
やbg-red-400
などの値が設定されているという感じです。
同じのをやってもメリットが分かりにくいので、intent
という状態の概念にsecondary
というプロパティを追加してみます。また、small
、medium
、large
という三つのプロパティを持つsize
という状態の概念を追加します。
import { cva } from "class-variance-authority";
const buttonStyles = cva('text-white', {
variants: {
intent: {
primary: 'bg-blue-400',
secondary: 'bg-gray-400',
danger: 'bg-red-400'
}
size: {
small: 'w-4 h-2',
medium: 'w-20 h-10',
large: 'w-40 h-20'
},
},
defaultVariants: {
intent: "primary",
size: "medium",
},
});
const Button = ({intent, size}) => {
return(
<button className={buttonStyles({intent, size})}>
Button
</button>
)
}
export default Button
Buttonコンポーネントにintent
とsize
という二つの状態の概念ができました。このコンポーネントは、propsにintent
とsize
を受け取り、その値によってデザインが適宜変更されるようになります。
これにより、条件分岐の選択肢が増えたとしても非常に管理がしやすい状態になりました。
また、ここでは紹介しませんがVariantProps
が提供されているため、これによりtypescriptにおける型補完の恩恵を享受できます。
詳しくは以下を見ると分かります。
Headless UIとの組み合わせ
最近では、Headless UIと呼ばれるStyleを持たない機能面だけのUIライブラリなどが出てきました。特に、tailwindlabsが公開しているheadless ui
は当然ながらtailwind cssとの組み合わせが最強なので、ぜひ使いましょう。
headless ui
では、as
というpropにはコンポーネントを使用でき、先ほど紹介したcva
で作成したコンポーネントを指定すると、intent='primary'
などが指定できるようになります。
import { Menu } from '@headlessui/react'
import { Button } from '@ui/Button'
const MyMenu = () => {
return (
<Menu>
<Menu.Button as={Button} intent='secondary' size='large'></Menu.Button>
<Menu.Items>
<Menu.Item href='hoge1'>hoge1</Menu.Item>
<Menu.Item href='hoge2'>hoge2</Menu.Item>
</Menu.Items>
</Menu>
)
}
export default MyMenu
このように、headless uiのコンポーネントにcva
で作ったコンポーネントをasで指定するとスタイルを適用できます。
最後に
cva
の良さは伝わったでしょうか?
条件分岐のCSSを指定させる方法として、CSSのクラス名をpropsとして渡すみたいな方法もあったのですが、cva
の方が個人的には良いと思います。もちろん、CSS in JSが嫌だという人はそうなってしまうと思うのですが、CSS in JSに抵抗がない人はぜひ使ってみてほしいなと思います。
やっぱり、コンポーネントの中にスタイルを入れることでメンテナンスはしやすいですしね。また、もちろん条件分岐が少ない場合はclsx
とかだけでも十分だと思います。しかし、ガッツリとデザインシステムを構築したいという人には、かなりベストな選択肢なのかなと今のところ思っています。
ここまで読んでくれて、ありがとうございます。
お疲れ様でした。
Discussion
少しデモを作ってみました。
定義側
使用側
簡単ですが、以上です。
class-variance-authorityライブラリを使ったより実践的な書き方は以下レポジトリが参考になります。
定義側
使用側
簡単ですが、以上です。
デモと実践例の紹介ありがとうございます!とても分かりやすかったです!
もはや、cvaの記事を書いていただきたいくらいです笑
shadcn/ui 良いですね!まさに、自分が作ろうかなと思っていたものでした😅