メインコンテンツまでスキップ

Tailwind CSS (React 統合パターン)

この章では、React 開発でデファクトとなっている Tailwind CSS を React と組み合わせる際のパターン に focus して学びます。

Tailwind CSS の体系的な学習は別ガイドへ

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
vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
plugins: [react(), tailwindcss()],
})
src/index.css
@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 で再利用ボタンを設計する

variantsize を Props で受け取り、対応するクラスをオブジェクトから引くパターンは Tailwind + React で最も多用されます。

src/components/Button.tsx
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 もこのパターンを採用しています。

src/lib/utils.ts
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
src/components/Button.tsx
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 ガイド の各章で扱います:

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 は本章の知識がそのまま活きるエコシステム

次に読む