レビュー待ち·難易度: 初級·更新: 2026-04-18

Tailwind CSS でフォーム部品(Input / Select / Checkbox / Radio)をスタイリングする

Tailwind CSS を使い Input / Select / Checkbox / Radio / Textarea などの基本フォーム部品に一貫したスタイルを適用するコンポーネント集。フォーカスリングやエラー状態のスタイルも示す。

nextjsstylingformtailwindcss

対応バージョン

nextjs 15react 19tailwindcss 4

前提環境

Tailwind CSS の基本的なクラスを理解していること

概要

Tailwind CSS で Input / Select / Checkbox / Radio / Textarea の基本フォーム部品にフォーカスリング・エラー状態・無効状態のスタイルを適用するコンポーネント集。再利用可能な部品として切り出すパターンも示す。

インストール

npm install tailwindcss

実装

テキスト Input

// components/form/TextInput.tsx
type Props = React.InputHTMLAttributes<HTMLInputElement> & {
  label: string;
  error?: string;
};

export function TextInput({ label, error, id, ...props }: Props) {
  const inputId = id ?? label;
  return (
    <div className="space-y-1">
      <label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
        {label}
      </label>
      <input
        id={inputId}
        className={`block w-full rounded border px-3 py-2 text-sm
          focus:outline-none focus:ring-2 focus:ring-blue-500
          disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-400
          ${error ? "border-red-400 bg-red-50" : "border-gray-300"}`}
        {...props}
      />
      {error && <p className="text-xs text-red-500">{error}</p>}
    </div>
  );
}

Select

// components/form/SelectInput.tsx
type Props = React.SelectHTMLAttributes<HTMLSelectElement> & {
  label: string;
  options: { value: string; label: string }[];
  error?: string;
};

export function SelectInput({ label, options, error, id, ...props }: Props) {
  const inputId = id ?? label;
  return (
    <div className="space-y-1">
      <label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
        {label}
      </label>
      <select
        id={inputId}
        className={`block w-full rounded border px-3 py-2 text-sm
          focus:outline-none focus:ring-2 focus:ring-blue-500
          disabled:cursor-not-allowed disabled:bg-gray-100
          ${error ? "border-red-400 bg-red-50" : "border-gray-300"}`}
        {...props}
      >
        <option value="">選択してください</option>
        {options.map((opt) => (
          <option key={opt.value} value={opt.value}>{opt.label}</option>
        ))}
      </select>
      {error && <p className="text-xs text-red-500">{error}</p>}
    </div>
  );
}

Checkbox

// components/form/CheckboxInput.tsx
type Props = React.InputHTMLAttributes<HTMLInputElement> & {
  label: string;
};

export function CheckboxInput({ label, id, ...props }: Props) {
  const inputId = id ?? label;
  return (
    <label htmlFor={inputId} className="flex cursor-pointer items-center gap-2">
      <input
        id={inputId}
        type="checkbox"
        className="h-4 w-4 rounded border-gray-300 text-blue-600
          focus:ring-2 focus:ring-blue-500 focus:ring-offset-0
          disabled:cursor-not-allowed disabled:opacity-50"
        {...props}
      />
      <span className="text-sm text-gray-700">{label}</span>
    </label>
  );
}

Radio グループ

// components/form/RadioGroup.tsx
type Option = { value: string; label: string };

type Props = {
  legend: string;
  name: string;
  options: Option[];
  value: string;
  onChange: (value: string) => void;
};

export function RadioGroup({ legend, name, options, value, onChange }: Props) {
  return (
    <fieldset className="space-y-2">
      <legend className="text-sm font-medium text-gray-700">{legend}</legend>
      {options.map((opt) => (
        <label key={opt.value} className="flex cursor-pointer items-center gap-2">
          <input
            type="radio"
            name={name}
            value={opt.value}
            checked={value === opt.value}
            onChange={() => onChange(opt.value)}
            className="h-4 w-4 border-gray-300 text-blue-600
              focus:ring-2 focus:ring-blue-500 focus:ring-offset-0"
          />
          <span className="text-sm text-gray-700">{opt.label}</span>
        </label>
      ))}
    </fieldset>
  );
}

Textarea

// components/form/TextareaInput.tsx
type Props = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
  label: string;
  error?: string;
};

export function TextareaInput({ label, error, id, ...props }: Props) {
  const inputId = id ?? label;
  return (
    <div className="space-y-1">
      <label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
        {label}
      </label>
      <textarea
        id={inputId}
        rows={4}
        className={`block w-full rounded border px-3 py-2 text-sm
          focus:outline-none focus:ring-2 focus:ring-blue-500
          disabled:cursor-not-allowed disabled:bg-gray-100
          ${error ? "border-red-400 bg-red-50" : "border-gray-300"}`}
        {...props}
      />
      {error && <p className="text-xs text-red-500">{error}</p>}
    </div>
  );
}

組み合わせフォーム例

// app/page.tsx
"use client";

import { useState } from "react";
import { TextInput } from "@/components/form/TextInput";
import { SelectInput } from "@/components/form/SelectInput";
import { CheckboxInput } from "@/components/form/CheckboxInput";
import { RadioGroup } from "@/components/form/RadioGroup";
import { TextareaInput } from "@/components/form/TextareaInput";

export default function Page() {
  const [plan, setPlan] = useState("free");

  return (
    <main className="mx-auto max-w-md p-8">
      <h1 className="mb-6 text-2xl font-bold">ユーザー登録</h1>
      <form className="space-y-5">
        <TextInput label="名前" placeholder="山田 太郎" />
        <TextInput label="メール" type="email" error="正しいメールアドレスを入力してください" />
        <SelectInput label="国" options={[{ value: "jp", label: "日本" }, { value: "us", label: "アメリカ" }]} />
        <RadioGroup
          legend="プラン"
          name="plan"
          options={[{ value: "free", label: "無料" }, { value: "pro", label: "Pro" }]}
          value={plan}
          onChange={setPlan}
        />
        <CheckboxInput label="利用規約に同意する" />
        <TextareaInput label="メモ" placeholder="任意で入力してください" />
        <TextInput label="無効フィールド" value="変更不可" disabled />
        <button type="submit" className="w-full rounded bg-blue-600 py-2 text-sm text-white hover:bg-blue-700">
          登録
        </button>
      </form>
    </main>
  );
}

ポイント

  • focus:ring-2 focus:ring-blue-500 focus:outline-none でフォーカスリングを統一する。outline-none でブラウザデフォルトを消してから Tailwind でリングを付け直す
  • エラー状態は border-red-400 bg-red-50 でボーダーと背景を変え、<p className="text-xs text-red-500"> でメッセージを表示する。error prop を追加するだけで切り替えられる設計にする
  • disabled: バリアントで無効時のスタイルをまとめて指定できる。cursor-not-allowed を付けると UX が向上する
  • Checkbox / Radio は <label><input> を囲むとクリック範囲が広がり操作しやすくなる
  • React Hook Form と組み合わせる場合は {...register("fieldName")} をスプレッドするだけで使える設計(...props を受け取る形式)

注意点

tailwind-badge-component はバッジ単体。tailwind-animation はアニメーション。tailwind-responsive-layout はブレークポイント。これは Input / Select / Checkbox / Radio など基本フォーム部品のスタイリング集(フォーカス・エラー状態含む)。

関連サンプル