Tailwindがデザイントークンを定義するのに向いている話
はじめに
Finatext アドベントカレンダー 21日目の記事です
こんにちは!Finatextのクレジットドメインでエンジニアをしている名澤(@studiokaiji)です。
この記事では、デザイントークンをTailwindで管理する方法と、そのメリットについてお話ししていきます。
そもそもデザイントークンってなんだっけ
デザイントークンとは、デザインシステムのうちのUIの基本的な要素(色、タイポグラフィ、スペーシング、アニメーションなど)を再利用可能な変数として定義しているものです。
これらを適切に管理することで以下のようなメリットがあります。
- UIの一貫性を保つことができる
- ブランドアイデンティティを強化できる
- 開発者間でのコミュニケーションを円滑にできる
なぜTailwindCSSがデザイントークン運用に適しているのか
TailwindCSSには、デザイントークンを運用する上で以下のような利点があります。
1. 柔軟な定義方法
configファイルだけでデザイントークンを定義できる
// tailwind.config.ts
import type { Config } from 'tailwindcss'
export default {
theme: {
colors: {
// カラーの定義
primary: {
DEFAULT: '#0066FF',
light: '#3385FF',
dark: '#0052CC',
},
success: '#4CAF50',
error: '#F44336',
},
spacing: {
// スペーシングの定義
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
fontSize: {
// フォントサイズの定義
body: ['16px', '1.5'],
h1: ['32px', '1.2'],
h2: ['24px', '1.3'],
},
},
} satisfies Config
既存のCSSカスタムプロパティとの連携も可能
/* styles/tokens.css */
:root {
--color-brand: #0066FF;
--spacing-unit: 8px;
}
// tailwind.config.ts with CSS variables
export default {
theme: {
colors: {
primary: 'var(--color-brand)',
},
spacing: {
unit: 'var(--spacing-unit)',
},
},
}
2. Tailwind自体のデザイントークンが活用できる
Tailwindには標準でデザイントークンが組み込まれています。これらをそのまま使用して徐々にカスタマイズを重ねたり、完全に上書きして独自のデザイントークンを定義することも可能です。
カスタマイズする場合
// tailwind.config.ts
import type { Config } from 'tailwindcss'
export default {
theme: {
extend: { // ← extendを付けると元のプロパティを継承できる
colors: {
primary: {
DEFAULT: '#0066FF',
light: '#3385FF',
dark: '#0052CC',
},
},
},
},
}
上書きする場合
export default {
theme: {
colors: { // 上書きしているので、colorsではprimaryしか使えない
primary: {
DEFAULT: '#0066FF',
light: '#3385FF',
dark: '#0052CC',
},
},
},
}
既にデザインシステムが存在している場合には、上書きして独自のデザイントークンを定義すればいいですし、ブランドカラーなど、プロジェクト共通で使う独自の変数だけを指定したい場合は、元のプロパティを使って開発を進めることができます。
実は、自分がTailwindがデザイントークンの定義に向いてると思う一番の理由が、この標準トークンの存在です。
全てにプロジェクトにおいて充実したデザインシステムが最初からあることの方が少なく、また新しくデザインシステムを作る場合でも、特にエンジニア主導で進める場合、1から全てを作るのは大変ですし、コード上だけでこれらを構築した結果、要求されたデザインを実現できないものになってしまったり、逆に自由度が高すぎて統一感のないデザインシステムになってしまう可能性もあります。
例えばTailwindでは、スペーシングが4px毎に数値が1大きくなるという規則性を持って定義されています。
そしてこの値はそのままclassとして1:1で紐づいているので、開発者にデザイントークンを使うことを自然に促すことができます(特に後述の拡張機能を使うとより使いやすくなります)。
つまり、Tailwindを使用することで、1からデザインに関する定義する必要がないというメリットに加えて、進んでデザイントークンを利用させるための仕組みと、それらをカスタムプロパティでも使えるという恩恵も受けることができます。
3. 導入方法の柔軟性
Tailwindはデザイントークンをclass名として定義して、それをstyleに変換するだけの比較的シンプルなライブラリです。
そのため、postcssとesbuildのようなバンドラを使える環境であれば、スタイルの記述先はHTMLでもCSSでもJSでも構わないため、例えばフロントでJSを使いたくないプロジェクトでも使用することができます。
逆にCSS in JS系のライブラリは、一見そのままCSSを記述できるものも多くシンプルに見えますが、基本的には使用しているフレームワークや言語(Javascript)に依存した実装が求められるため、移行や別プロジェクトでの使い回しに課題があります。
そのほかにも、具体的なトークン(セマンティックトークン)だけをJS側で定義するといったことも可能なため、呼び出し側からはJSを使ってstyle定義をすることも可能です。
export const tokens = {
colors: {
primary: 'bg-blue-500',
secondary: 'bg-gray-500',
success: 'bg-green-500',
error: 'bg-red-500',
},
spacing: {
small: 'p-2',
medium: 'p-4',
large: 'p-6',
},
};
4. 拡張機能
TailwindにはVSCodeの拡張機能があり、以下のようにclass名のサジェストを出してくれます。
これだけでも便利ですが、なんとこのサジェストはカスタムプロパティでも出てくるので、コンポーネント実装のたびにデザイントークンの定義ファイルを見にいく必要がありません🙌
他ライブラリとの比較
ここからは実際に他ライブラリと比較してみて、Tailwindが向いている/向いてないケースを洗い出していきます。
特徴 | Tailwind CSS | Style Dictionary | CSS Custom Properties | CSS-in-JS |
---|---|---|---|---|
導入の容易さ | ◎ 最小限の設定で開始可能 | △ 設定ファイルの記述が複雑 | ○ 追加の設定不要 | ○ 比較的容易 |
標準デザイントークン | ◎ 豊富な標準セット | × なし | × なし | △ ライブラリによる |
型安全性 | ○ プラグインで対応可能 | ○ 設定可能 | × なし | ◎ 標準でサポート |
エディタサポート | ◎ 優れた補完機能 | △ 限定的 | △ 限定的 | ○ 一般的に良好 |
パフォーマンス | ◎ ビルド時に最適化 | ○ ビルド時生成 | ◎ ネイティブ機能 | △ ランタイムオーバーヘッドあり |
カスタマイズ性 | ◎ 高い | ◎ 高い | ○ 中程度 | ◎ 高い |
デザインツール連携 | △ 限定的 | ◎ 優れている | × 手動 | △ 限定的 |
フレームワーク依存 | × なし | × なし | × なし | ○ あり |
ビルドサイズ | ○ 最適化可能 | ◎ 小さい | ◎ 最小限 | △ 比較的大きい |
動的な値の変更 | △ 制限あり | × 困難 | ◎ 容易 | ◎ 容易 |
向いているケース
- プロジェクトの特徴
- 新規プロジェクトの立ち上げ
- 中小規模のプロジェクト
- 短期間での開発が求められる案件
- マイクロフロントエンド環境
- チーム構成
- フロントエンド/バックエンドの垣根なく開発するチーム
- CSS設計の知識が浅いメンバーが多いチーム
- 少人数での開発体制
- 要件
- デザインシステムを段階的に構築したい
- 標準的なUIコンポーネントを多用する
- プロトタイプの素早い作成が必要
- フレームワークに依存したくない
- 技術的な条件
- ビルド時の最適化を重視
- エディタのサポートを活用したい
- CSSの詳細度の問題を避けたい
向いていないケース
- プロジェクトの特徴
- レガシーコードベースへの部分的な導入
- 大規模で複雑なアニメーションを多用するサイト
- デザインの自由度が非常に高い必要がある案件
- パフォーマンスが極めて重要なAMP対応サイト
- チーム構成
- CSSに精通した開発者が多いチーム
- デザイナーが直接CSSを編集する必要がある環境
- BEMなどの既存のCSS設計手法が定着している
- 要件
- 動的なテーマ切り替えが頻繁に必要
- デザインツールとの緊密な連携が必須
- 独自の複雑なデザインシステムが既に存在する
- CSS変数の動的な操作が多い
- 技術的な条件
- JavaScriptを使用できない環境
- ビルドプロセスを導入できない環境
- インラインスタイルを避けたい場合
- CSSファイルサイズを極限まで抑えたい場合
他ライブラリと比較してみると、Tailwindが、複雑なアニメーションが多いケースや既存のCSS設計があるような複雑なプロジェクトに向いていない反面、スモールスタートでスピードと一貫したデザインシステムの構築には向いていることがわかりました。
しかし、Tailwind自体のHTMLやJSXに直接スタイルを記述していく方式や、ライブラリとしての歴史の浅さから、開発が止まってしまい技術的負債になってしまうのではないかという懸念もよく聞きます。
そこで、次のセクションでは、技術的負債にならない実装方針を考えていきます。
Tailwindをできるだけ負債にしないコンポーネントの実装方針
プリミティブな要素が適切にコンポーネント化されているプロジェクトにおいて、Tailwindが技術的負債になりやすい場面は、いわゆるページ側(Atomic Designで言えばTemplatesやPages)でレイアウトに関する記述をする時だと思います。
これらは、シンプルなmarginもあればflexやgridを使った複雑なリストもあるかもしれません。
また、コンポーネント化されている要素と違い、各ページやコンポーネントなどにそれぞれ散らばっており、これらを置き換えるのは大きなプロジェクトの場合それなりの労力がかかります。
そこで、この懸念を払拭するために以下のような実装方針を考えてみました。
classやstyleを受け取らない
ページ側で直接スタイル記述ができると定義が散らばってしまうので、これらのプロパティは受け取らないでください。
またこれらのプロパティは、デザイントークンの定義に反した記述が簡単にできてしまうので、カスタムコンポーネントに限らず、デザインシステムを利用するプロジェクト全体で、特別な理由がない限り使用を禁止すべきです。
marginなどはpropsのプロパティとして受け取る
marginなどのプロパティはpropsに渡せるようにしておくことで、親要素から配置を制御できます。
<Button mt={2} mb={2}>ここをクリック</Button> // margin-top, margin-bottomを指定
classやstyleを受け取る方式に比べると、以下のようなメリットがあります。
- 受け取るプロパティを制限できる
- デザイントークンに反した値の指定を禁止できる
- 他のスタイリングライブラリに移行する際、propsのインターフェースを変える必要がない(=ページ側の変更がない)
MUIをはじめとして多くのUIコンポーネントライブラリもこの方針で実装しているので、初めてあなたのデザインシステムとUIコンポーネントライブラリを使って実装する開発者にとっても、わかりやすくなると思います。
Layoutコンポーネントを実装する
flexやgridといった、小要素の配置や余白を親から制御するようなレイアウトは、それ単体でコンポーネント化することが望ましいです。
また受け取るプロパティも上記のように制限することで、統一感を崩さずに複雑なレイアウトを組むことができます。
const spacingStyles = {
none: 'gap-0',
xs: 'gap-1',
sm: 'gap-2',
md: 'gap-4',
lg: 'gap-6',
xl: 'gap-8',
}
const alignStyles = {
start: 'items-start',
end: 'items-end',
center: 'items-center',
stretch: 'items-stretch',
}
const justifyStyles = {
start: 'justify-start',
end: 'justify-end',
center: 'justify-center',
between: 'justify-between',
}
type VStackProps = {
children: React.ReactNode
spacing?: keyof typeof spacingStyles
align?: keyof typeof alignStyles
}
// 縦並び
const VStack = ({
children,
spacing = 'none',
align = 'stretch',
}: VStackProps) => {
return (
<div
className={`
flex flex-col
${spacingStyles[spacing]}
${alignStyles[align]}
`}
>
{children}
</div>
)
}
type HStackProps = {
children: React.ReactNode
spacing?: keyof typeof spacingStyles
justify?: keyof typeof justifyStyles
align?: keyof typeof alignStyles
}
// 横並び
const HStack = ({
children,
spacing = 'none',
justify = 'start',
align = 'center',
}: HStackProps) => {
return (
<div
className={`
flex flex-row
${spacingStyles[spacing]}
${justifyStyles[justify]}
${alignStyles[align]}
`}
>
{children}
</div>
)
}
ここで書いている方針は、Tailwindに限らず他のスタイリングライブラリを使う場合にも言えることであり、長期間そのコードベースを利用し続けるならできるだけ守りたいポイントでもあります。
まとめ
今回は、Tailwindを使ってデザイントークンを管理するメリットと、できるだけ技術的負債にならないようにする方法を合わせて紹介してきました。
デザインシステムを構築する上で、デザイントークンの定義・管理は軸になる部分であり、開発効率だけでなく、デザインチームとの連携の取りやすさ、保守性、デザインのユビキタス言語としての役割など、様々な要素を考慮して技術選定・定義をする必要があります。
今回はTailwindを使った定義・管理方法を紹介しましたが、そのほかにもStyle DictionaryとCSS Moduleを使ったり、既存のUIライブラリをカスタマイズして独自ライブラリを作るなど、デザインシステムを構築する手段はいくつもあるので、是非他の方法も調べてみて、会社やプロジェクトに適した仕組みを検討してみてください!
Discussion