🥰
tailwindcssで美しいデザインシステムを構築した話
デザインシステムとは、「あるべきデザインを一貫性を持ってユーザーに提供するための仕組み」である。(デジタル庁デザインシステムより引用)
デザインシステムには、Figma等で描かれたスタイルガイドやコンポーネントだけでなく、ソースコードも含まれる。むしろ、デザインの一貫性を担保するにはソースコードの中身が重要なのは言うまでもない。
今回は、tailwindcss(以下tailwind)を採用したプロジェクトで美しいデザインシステムを構築することができたので、その際に意識したことを紹介する。
1. tailwindでプリミティブなスタイルを提供する
- プリミティブ=原始的。これ以上細かい単位でCSSを当てることはできない。
- marginやwidthなど、全てのCSSに制約を設定するのは現実的ではないが、tailwindのデフォルトの制約が丁度よかった。
- color, fontSizeなどはデザインに合わせてtailwind.configで使えるCSSを制御する。
- 以下にファイルの中身のイメージを示す(数値はダミーデータ)
tailwind.config.js
module.exports = {
theme: {
// <https://tailwindcss.com/docs/customizing-colors>
colors: {
transparent: "transparent",
current: "currentColor",
white: "#ffffff",
black: {
50: "#f8fafc",
100: "#f1f5f9",
200: "#e2e8f0",
300: "#cbd5e1",
400: "#94a3b8",
500: "#64748b",
600: "#475569",
700: "#334155",
800: "#1e293b",
900: "#0f172a",
1000: "#020617",
},
},
// <https://tailwindcss.com/docs/font-size#customizing-your-theme>
fontSize: {
xs: [
"10px",
{
lineHeight: "16px",
letterSpacing: "-0.02em",
},
],
sm: [
"12px",
{
lineHeight: "20px",
letterSpacing: "-0.02em",
},
],
base: [
"16px",
{
lineHeight: "30px",
letterSpacing: "-0.02em",
},
],
lg: [
"18px",
{
lineHeight: "32px",
letterSpacing: "-0.02em",
},
],
xl: [
"24px",
{
lineHeight: "36px",
letterSpacing: "-0.02em",
},
],
"2xl": [
"40px",
{
lineHeight: "64px",
letterSpacing: "-0.02em",
},
],
},
fontFamily: {
gothic: `"游ゴシック体", "Yu Gothic", YuGothic, "ヒラギノ角ゴ Pro", "Hiragino Kaku Gothic Pro", "メイリオ", Meiryo, "MS Pゴシック", "MS PGothic", sans-serif`,
mincho: `"游明朝体", "Yu Mincho", YuMincho, "ヒラギノ明朝 Pro", "Hiragino Mincho Pro", "MS P明朝", "MS PMincho", serif`,
},
2. 基本コンポーネントでセマンティックなスタイルを提供する
- セマンティック=意味的。ButtonやTextなど、UIとして我々が用途を理解できる単位。
- propsとしてsizeやvariantを受け取り、それに応じたスタイルが当たるようにする。コンポーネントの命名やpropsはChakra UIを大いに参考にした。
- このレイヤーはドメイン知識は含まない(別プロジェクトでもコンポーネントを使い回せるくらいにする)。
- 以下に一例としてLinkコンポーネントを示す。
import { twJoin, twMerge } from "tailwind-merge";
import { FunctionComponent, PropsWithChildren } from "react";
export type LinkProps = {
size?: "sm" | "md";
};
const baseClass = twJoin(
"text-black-700 underline underline-offset-2 duration-150",
"visited:text-black-700",
"hover:text-black-300",
);
const sizeClass: Record<NonNullable<LinkProps["size"]>, string> = {
md: twJoin("text-md"),
sm: twJoin("text-sm"),
};
export const Link: FunctionComponent<PropsWithChildren<LinkProps>> = ({
size,
children,
...props
}) => {
return (
<a className={twMerge(baseClass, size && sizeClass[size])} {...props}>
{children}
</a>
);
};
ここまで来たら、あとは実際のアプリの見た目や機能ごとに分けられた「通常コンポーネント」に、基本コンポーネントをレイアウトするだけである。
感想
技術選定について
- Chakra UIなどのコンポーネントライブラリは、元々ある見た目に引っ張られてしまったりデザインが制御しきるのが大変という経験があった。今回はデザイナーが最初からチームにいたため、自分たちで1からデザインを作れることを優先した。結果的に、基本コンポーネントの機能が必要最低限なため、美しいソースコードになったと思う。
- 初めはvanilla-extractを導入していたが、私と上司が別プロジェクトでChakra UIを使用していたのもあり、jsxとcssの2つのファイルを行き来する体験が合わなかった。また、vanilla-extractでもcss変数を定義できるが、前述したようにmarginやwidthまで制約を記述するのは面倒である。tailwindはあらかじめプリミティブな制約が課されているので、その制約に適合するデザインであれば、迷いなくスタイルを当てることができる。
tailwindについて
- tailwindはソースコードがclassだらけで見辛くなると言う意見があるが、上記のような使い方をすることで、色や大きさなどに関するclassは基本コンポーネント、余白や配置に関するclassは通常コンポーネントに集約される。責務が明確なため、何が書かれているかも読みやすい。
- また、clsxやtwMergeを使うことで、classを改行したり変数に入れて結合したりできるので、可読性をあげられる。以下にTabコンポーネントのclassを示す。形状に関するclass, 文字に関するclass, 擬似クラス付きclassと、まとまりごとに改行されている。
const baseClass = twJoin(
"h-10 flex-shrink-0 px-2 py-3 duration-150",
"font-gothic text-sm",
"border-b border-black-100 bg-white text-black-400",
"hover:border-b hover:border-black-700 hover:bg-black-100 hover:text-black-700",
"aria-selected:border-b-2 aria-selected:border-black-700 aria-selected:bg-white aria-selected:text-black-700",
);
- tailwindのclass名がやや独特(justify-xxとかtext-xx/font-xxの使い分けとか)という懸念があったが、copilotがあればいい感じにアシストしてくれるので気にならない。また、prettier-plugin-tailwindcssがclassをソートしてくれるのだが、存在しないclassは一番前にソートされるので間違いにも気づきやすかった。
Discussion