ウェブエンジニア問題集

ReactとTailwindの組み合わせ実践

TailwindとReactを組み合わせると、長いクラス列の問題がコンポーネント化で解決できます。この章では実務でよく使うパターン(cn()cva・動的クラスの注意点)をまとめます。

学習者学習者

ボタンの色やサイズをpropsで切り替えたいけど、クラスを条件分岐で組み立てると undefined が混ざったり競合したり…うまくいかない。

先生先生

そこで登場するのが cn()cva。条件付きのクラス結合と、バリアントの型安全な管理を任せられるんだ。この章で一気に解決しよう。

ReactとTailwindをうまく使いこなせたイメージ

cn():クラスの条件付き結合

clsxtailwind-merge を組み合わせた cn() 関数は、条件付きクラス指定と競合解決を同時に行えます。

npm install clsx tailwind-merge
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
import { cn } from '@/lib/utils'
 
function Button({ disabled, className }: { disabled?: boolean; className?: string }) {
  return (
    <button
      className={cn(
        'px-4 py-2 bg-blue-600 text-white rounded-lg font-semibold transition-colors',
        disabled && 'opacity-50 cursor-not-allowed',
        className,
      )}
    >
      送信
    </button>
  )
}

cva:バリアント管理

class-variance-authority でコンポーネントのバリアントを型安全に管理できます。

npm install class-variance-authority
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
 
const buttonVariants = cva(
  // ベースクラス
  'inline-flex items-center justify-center gap-2 rounded-lg font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:opacity-50 disabled:cursor-not-allowed',
  {
    variants: {
      variant: {
        primary:  'bg-blue-600 hover:bg-blue-700 text-white focus-visible:ring-blue-500',
        outline:  'border border-blue-600 text-blue-600 hover:bg-blue-50 focus-visible:ring-blue-500',
        ghost:    'text-gray-600 hover:bg-gray-100 hover:text-gray-900',
        danger:   'bg-red-600 hover:bg-red-700 text-white focus-visible:ring-red-500',
      },
      size: {
        sm: 'px-3 py-1.5 text-sm',
        md: 'px-4 py-2 text-sm',
        lg: 'px-5 py-2.5 text-base',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
)
 
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof buttonVariants>
 
export function Button({ variant, size, className, ...props }: ButtonProps) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}
// 使い方
<Button variant="primary" size="lg">送信</Button>
<Button variant="outline" size="sm">キャンセル</Button>
<Button variant="danger">削除</Button>

動的クラスの注意点

TailwindはCSSのビルド時にHTMLやJSXをスキャンしてクラスを収集します。そのため、動的に文字列を組み立てると認識されません。

// NG:動的に組み立てると未収集になる
const color = 'blue'
<div className={`bg-${color}-600`}>NG</div>
 
// OK:クラス名を完全な文字列で書く
const colorMap = {
  blue: 'bg-blue-600',
  red:  'bg-red-600',
}
<div className={colorMap[color]}>OK</div>

shadcn/ui との組み合わせ

shadcn/ui はTailwind + Radix UIをベースにしたコンポーネントライブラリです。コンポーネントのソースコードを直接プロジェクトに追加するため、自由にカスタマイズできます。

npx shadcn@latest init
npx shadcn@latest add button card dialog

cn()cva を内部で使っており、本章の知識がそのまま活きます。


ちゃんと使うためのポイント

  • cn() はTailwindをReactで使うなら必ず導入する
  • バリアントが増えたら cva でまとめて管理する
  • クラス名は完全な文字列で書く(動的な文字列結合は避ける)
  • shadcn/ui はカスタマイズ性が高くおすすめのコンポーネントベース

TailwindとReactを組み合わせることで、型安全かつ保守しやすいコンポーネント指向のスタイリングが実現できます。Reactそのものをさらに深めたい場合はReact入門もあわせてどうぞ。引き続きクイズでも知識を定着させましょう。