Tailwind CSS (React 統合パターン)
この章では、React 開発でデファクトとなっている Tailwind CSS を React と組み合わせる際のパターン に focus して学びます。
Tailwind の基礎 (セットアップ、ユーティリティファースト、Flexbox / Grid / Typography / Colors 等) は Tailwind CSS ガイド (全 18 章) で体系的に解説します。
本章は React に固有のトピック (variants 設計 / clsx / tailwind-merge / cn / shadcn/ui) のみを扱います。Tailwind 自体が初めての場合は、まず Tailwind ガイドの基礎とレイアウトの章を読むことをおすすめします。同ガイドの公開までは、本章のセットアップ要点で十分です。
この章で学ぶこと
- React で Tailwind を使う際のセットアップ要点 (Tailwind ガイドへのリンクで補完)
classNameへの動的クラス指定パターンvariant×sizeで再利用可能なボタン設計clsxで条件付きクラスを整理tailwind-mergeでクラス衝突を解決cnユーティリティ (shadcn/ui 標準パターン)
セットアップ要点
Vite + React プロジェクトでは @tailwindcss/vite プラグインを使用します (v4 以降は tailwind.config.js 不要)。
npm install tailwindcss @tailwindcss/vite
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})
@import "tailwindcss";
詳しいセットアップ手順 (PostCSS / CLI / CDN / VS Code 拡張 / Prettier 連携 / Next.js / Nuxt 用) は Tailwind CSS ガイドの環境構築章 で解説します。
React で className に動的クラスを書く基本
JSX では class ではなく className 属性を使います。<style> を書かず、ユーティリティクラスを className に連結するのが Tailwind + React の基本形です。
function ProfileCard({ name, isOnline }: { name: string; isOnline: boolean }) {
return (
<div className="flex items-center gap-3 p-4 bg-white rounded-lg shadow-sm">
<div className={`w-10 h-10 rounded-full ${isOnline ? 'bg-green-500' : 'bg-gray-300'}`} />
<span className="font-medium text-gray-900">{name}</span>
</div>
)
}
テンプレートリテラルでの三項演算子は最低限なら OK ですが、条件が増えると読みづらくなります。次節の clsx / cn で整理します。
variant × size で再利用ボタンを設計する
variant と size を Props で受け取り、対応するクラスをオブジェクトから引くパターンは Tailwind + React で最も多用されます。
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
children: React.ReactNode
onClick?: () => void
}
const baseClasses = 'rounded-sm font-medium transition-colors focus:outline-hidden focus:ring-2'
const sizeClasses = {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
} satisfies Record<NonNullable<ButtonProps['size']>, string>
const variantClasses = {
primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-300',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-300',
danger: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-300',
} satisfies Record<NonNullable<ButtonProps['variant']>, string>
export function Button({
variant = 'primary',
size = 'md',
disabled = false,
children,
onClick,
}: ButtonProps) {
const disabledClasses = disabled ? 'opacity-50 cursor-not-allowed' : ''
return (
<button
className={`${baseClasses} ${sizeClasses[size]} ${variantClasses[variant]} ${disabledClasses}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
)
}
ポイント:
satisfies Record<...>で キーの網羅性 を型レベルで強制。新しいvariantを追加し忘れるとコンパイルエラーbaseClasses/sizeClasses/variantClassesを分けて、責務を明示- 大きくなったら次節の
cva(class-variance-authority) に移行する
clsx — 条件付きクラスをきれいに
clsx は条件に基づいてクラス名を結合するシンプルなユーティリティ。falsy 値 (false / null / undefined / 0 / "") は無視されます。
npm install clsx
import clsx from 'clsx'
function Button({ isActive, isDisabled, children }: {
isActive: boolean
isDisabled: boolean
children: React.ReactNode
}) {
return (
<button
className={clsx(
'px-4 py-2 rounded-sm',
isActive && 'bg-blue-500 text-white',
!isActive && 'bg-gray-200 text-gray-800',
isDisabled && 'opacity-50 cursor-not-allowed'
)}
disabled={isDisabled}
>
{children}
</button>
)
}
clsx(['base', { 'bg-blue-500': isActive }]) のようにオブジェクト記法も使えます。類似ライブラリ classnames のより軽量・高速な drop-in replacement (公式 README で 239B) として、Tailwind コミュニティで事実上の標準です。
tailwind-merge — クラス衝突を解決
Tailwind では同じプロパティのクラスが重複すると CSS の生成順 に依存し、後勝ちが保証されません。
// 期待: bg-red-500 が適用
// 実際: CSS の順序次第で bg-blue-500 が勝つ可能性あり
<div className="bg-blue-500 bg-red-500" />
これは特に「親が渡したクラスを子で上書きしたい」場面 (再利用コンポーネントの拡張) で頻発します。
npm install tailwind-merge
import { twMerge } from 'tailwind-merge'
twMerge('bg-blue-500', 'bg-red-500')
// => 'bg-red-500'
twMerge('px-2 py-1', 'p-4')
// => 'p-4' (px-2 py-1 は p-4 に吸収される)
cn ユーティリティ — clsx + tailwind-merge
実際は clsx (条件付き組み立て) と tailwind-merge (衝突解決) を組み合わせた cn 関数を作るのが定番です。shadcn/ui もこのパターンを採用しています。
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs))
}
import { cn } from '@/lib/utils'
type CardProps = {
variant?: 'default' | 'highlighted'
className?: string
children: React.ReactNode
}
export function Card({ variant = 'default', className, children }: CardProps) {
return (
<div
className={cn(
'p-4 rounded-lg shadow-sm',
variant === 'default' && 'bg-white',
variant === 'highlighted' && 'bg-blue-50 ring-2 ring-blue-500',
className // 親が渡した className が最後勝ち
)}
>
{children}
</div>
)
}
// 使う側
<Card variant="default" className="bg-gray-100"> {/* bg-white → bg-gray-100 が確実に勝つ */}
...
</Card>
class-variance-authority (cva) — variant 設計を宣言的に
variant × size パターンが複雑化したら、専用ライブラリ class-variance-authority (cva) で宣言的に書けます。
npm install class-variance-authority
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'rounded-sm font-medium transition-colors focus:outline-hidden focus:ring-2',
{
variants: {
variant: {
primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-300',
secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300 focus:ring-gray-300',
danger: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-300',
},
size: {
sm: 'px-2 py-1 text-sm',
md: 'px-4 py-2',
lg: 'px-6 py-3 text-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
)
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
VariantProps<typeof buttonVariants>
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button className={cn(buttonVariants({ variant, size }), className)} {...props} />
)
}
cva の良いところ:
- 型推論:
VariantProps<typeof buttonVariants>でvariant/sizeの型が自動生成される - デフォルト値:
defaultVariantsで省略時の挙動を宣言 - compoundVariants: 複数 variant の組み合わせ時のスタイルも記述可能
- shadcn/ui 内部でも採用されているデファクト
shadcn/ui との連携
shadcn/ui は Tailwind CSS ベースのコンポーネントライブラリで、コンポーネントを自プロジェクトにコピーする 形式が特徴です。npm install ではなく npx shadcn@latest add button のようにソースをローカルに取り込みます。
利点:
- ライブラリのバージョン互換に縛られない (自分のコードとして所有)
- Tailwind の
@themeで色を変えるだけでテーマ全体が変わる cn/cvaを内部で使っており、本章の知識がそのまま活きる
shadcn/ui を採用する場合、本章の Button を自作する代わりに npx shadcn@latest add button で生成し、必要に応じて variants を追加するのが一般的です。
レスポンシブと状態バリアント (React 視点)
Tailwind のレスポンシブ (sm: / md: / lg:) と状態バリアント (hover: / focus: / active: / disabled:) は React でもそのまま使えます。詳細は Tailwind CSS ガイド の各章で扱います:
- レスポンシブデザイン —
sm:md:の基本 - 状態バリアント —
hoverfocusgroup-*peer-*has-* - ダークモード —
dark:バリアントと切り替え方式
React 固有の注意点として、disabled 属性を付けたボタンは disabled:opacity-50 のような Tailwind バリアントと組み合わせると、HTML レベルで disabled な要素のスタイルを宣言的に切り替えられます。
<button
className="px-4 py-2 bg-blue-500 text-white rounded-sm disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
送信
</button>
SSR / RSC での注意
Next.js App Router など RSC (React Server Components) で Tailwind を使う場合、ユーティリティクラスはサーバーレンダリング時にそのまま HTML に出力されるため、特別な配慮は不要です。ただし clsx / cn を使う関数を client component / server component の両方で共通利用 する場合、'use client' ディレクティブの伝播を意識する必要があります (純粋な文字列結合関数は server safe)。
まとめ
- 基礎は Tailwind CSS ガイド に集約、本章は React 統合パターン に focus
variant×sizeのオブジェクト引きパターン +satisfies Recordで型安全にclsxで条件付きクラス、tailwind-mergeで衝突解決、cnで両者を統合- 大規模化したら
class-variance-authority(cva) で宣言的に - shadcn/ui は本章の知識がそのまま活きるエコシステム
次に読む
- Tailwind CSS ガイド — Tailwind 自体を体系的に学ぶ
- PortalとModal — createPortal でモーダルを実装する