概要
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">でメッセージを表示する。errorprop を追加するだけで切り替えられる設計にする disabled:バリアントで無効時のスタイルをまとめて指定できる。cursor-not-allowedを付けると UX が向上する- Checkbox / Radio は
<label>で<input>を囲むとクリック範囲が広がり操作しやすくなる - React Hook Form と組み合わせる場合は
{...register("fieldName")}をスプレッドするだけで使える設計(...propsを受け取る形式)