【保存版】Laravelフォームバリデーション完全攻略:アクセシブルなエラーメッセージと入力体験を実装する実務ガイド
この記事で学べること(先に要点)
- FormRequest を用いた堅牢なサーバーサイドバリデーションの書き方
- 多言語対応(日本語/英語など)でわかりやすいエラーメッセージを設計する方法
- Blade コンポーネント化で「ラベル・ヘルプ・エラー」を一体管理し、ARIA属性を正しく付与する実装
- 非同期(AJAX)バリデーションとフォーム送信後のフォーカス/読み上げ制御
- マルチステップ・ファイルアップロード・ユニークチェック等の実践パターン
- だれにとっても使いやすい入力体験を実現するアクセシビリティ・チェックリスト
想定読者(だれが得をする?)
- Laravel 初~中級のエンジニア:入力フォームを「とりあえず動く」から「本番品質」に引き上げたい方
- 受託/社内開発のテックリード:チーム標準のフォームコンポーネントと運用ルールを整えたい方
- CS/サポート担当・PO:エラーで離脱しない、問い合わせが減るフォームUXを実装方針から把握したい方
- デザイナー/QA:色・コントラスト・キーボード操作・読み上げなど、UI仕様と実装の橋渡しをしたい方
アクセシビリティレベル:★★★★☆
- フォーム要素の関連付け(
label
/for
・id
)・aria-describedby
・role="alert"
・aria-live
・aria-invalid
を実装レベルで提示 - キーボード操作・フォーカス移動・エラー出力領域のライブ通知・コントラストとステート表示を明示
- 読み上げ・視覚・触覚(フォーカスリング)への重層的配慮を網羅(ただし音声入力や点字端末の実機検証は範囲外のため星4)
1. はじめに:フォームUXの原則と Laravel での実現ポイント
Webフォームは多くのサービスで「登録・購入・申請」といった最重要フローの入口です。スムーズな入力体験はコンバージョンや顧客満足に直結し、逆に分かりづらいエラーメッセージや不親切な入力制御は離脱の主要因になります。Laravel は強力なバリデーション機構を備え、サーバーサイドの正確性とアクセシビリティを満たすUIを両立しやすいフレームワークです。
本記事では「正しい入力」「わかるエラー」「すぐ直せる導線」の3要素を柱に、FormRequest・Blade コンポーネント・多言語化・非同期バリデーション・マルチステップなど、実務で頻出の設計と実装を丁寧に解説します。コードはコピペして動かせる粒度で掲載しますので、今日からプロジェクトに取り入れてくださいね♡
2. サーバーサイドの要:FormRequest でルールを集中管理
2.1 FormRequest を作る
まずは登録フォームを例に、php artisan
でリクエストクラスを作成します。
php artisan make:request RegisterUserRequest
// app/Http/Requests/RegisterUserRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RegisterUserRequest extends FormRequest
{
public function authorize(): bool
{
// 認可は別途管理。通常は true でOK
return true;
}
public function rules(): array
{
return [
// bail で最初の失敗で以降のルール評価を止め、冗長なエラーを避ける
'name' => ['bail','required','string','max:50'],
'email' => ['bail','required','email','max:255', Rule::unique('users','email')],
'password' => ['bail','required','string','min:8','confirmed'], // password_confirmation と一致
'agree' => ['accepted'], // 規約同意チェックボックス
// 任意項目
'phone' => ['nullable','string','max:20'],
];
}
// 属性名をやさしい日本語に
public function attributes(): array
{
return [
'name' => 'お名前',
'email' => 'メールアドレス',
'password' => 'パスワード',
'password_confirmation' => 'パスワード(確認)',
'agree' => '利用規約',
'phone' => '電話番号',
];
}
// 追加の日本語メッセージ(必要な分だけ)
public function messages(): array
{
return [
'email.unique' => ':attribute は既に登録されています。',
'agree.accepted' => ':attribute への同意が必要です。',
'password.confirmed' => ':attribute が一致しません。',
];
}
// 事前整形:前後空白除去など
protected function prepareForValidation(): void
{
$this->merge([
'email' => is_string($this->email) ? trim($this->email) : $this->email,
'name' => is_string($this->name) ? trim($this->name) : $this->name,
]);
}
}
ポイント
bail
:同一項目にエラーが多発すると読みにくいので、最初の失敗で打ち切り。attributes()
:メッセージ中の項目名を自然言語化(「email → メールアドレス」)。prepareForValidation()
:空白除去やフォーマット正規化はここで。nullable
/sometimes
:任意項目の扱いを明確に。空欄を許容するならnullable
。
2.2 コントローラでの利用
// app/Http/Controllers/Auth/RegisterController.php
use App\Http\Requests\RegisterUserRequest;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class RegisterController
{
public function store(RegisterUserRequest $request)
{
$user = User::create([
'name' => $request->string('name'),
'email' => $request->string('email'),
'password' => Hash::make($request->string('password')),
'phone' => $request->string('phone'),
]);
// ログイン・リダイレクトなど
auth()->login($user);
return redirect()->route('dashboard')
->with('status', 'ご登録ありがとうございます!');
}
}
- FormRequest をタイプヒントすれば、検証済みのデータのみが届きます。
string('field')
で型保証された値を取り出し、意図せぬ配列注入を防止します。
3. UI の要:アクセシブルな Blade 入力コンポーネント
毎回 HTML を手書きするとラベルや aria
の付け忘れが起こりがち。「ラベル・ヘルプ・エラー表示」をワンセットにしたコンポーネントを用意しましょう。
3.1 テキスト入力 <x-form.input />
{{-- resources/views/components/form/input.blade.php --}}
@props([
'id',
'label',
'type' => 'text',
'name' => null,
'help' => null,
'required' => false,
'autocomplete' => null,
'inputmode' => null,
])
@php
$name = $name ?? $id;
$error = $errors->first($name);
$describedBy = trim(($help ? $id.'-help ' : '') . ($error ? $id.'-error' : ''));
@endphp
<div class="mb-5">
<label for="{{ $id }}" class="block font-medium">
{{ $label }}
@if($required)
<span class="text-red-600" aria-hidden="true">*</span>
@endif
</label>
<input
id="{{ $id }}"
name="{{ $name }}"
type="{{ $type }}"
value="{{ old($name) }}"
@if($required) required aria-required="true" @endif
@if($autocomplete) autocomplete="{{ $autocomplete }}" @endif
@if($inputmode) inputmode="{{ $inputmode }}" @endif
@if($describedBy) aria-describedby="{{ $describedBy }}" @endif
@class([
'mt-1 block w-full rounded border px-3 py-2',
'border-gray-300 focus:ring-2 focus:ring-blue-600 focus:border-blue-600' => !$error,
'border-red-600 focus:ring-2 focus:ring-red-600' => $error,
])
@if($error) aria-invalid="true" @endif
/>
@if($help)
<p id="{{ $id }}-help" class="text-sm text-gray-600 mt-1">
{{ $help }}
</p>
@endif
@if($error)
<p id="{{ $id }}-error" class="text-sm text-red-700 mt-1" role="alert">
{{ $error }}
</p>
@endif
</div>
3.2 メール/パスワードに適用
<x-form.input id="name" label="お名前" required autocomplete="name" />
<x-form.input id="email" type="email" label="メールアドレス" required autocomplete="email" />
<x-form.input id="password" type="password" label="パスワード" required autocomplete="new-password" />
<x-form.input id="password_confirmation" type="password" label="パスワード(確認)" required autocomplete="new-password" />
ポイント
aria-describedby
に ヘルプとエラーの両方を関連付け。- エラー時は
aria-invalid="true"
と赤系のボーダー/テキストで状態を多重表現(色だけに依存しない)。 autocomplete
とinputmode
で入力支援(例:電話ならinputmode="numeric"
)。
4. エラーメッセージの設計:短く具体的、直せる言葉で
4.1 伝わるメッセージの原則
- 短く:「有効なメールアドレスを入力してください」
- 具体的:パスワード要件(例:8文字以上・英数字)を事前に提示
- 項目名は自然言語:
attributes()
で日本語化 - 視認性:赤色 + アイコン +
role="alert"
で音声読み上げにも対応 - フィールド直下に出す:視線移動を最小化
4.2 多言語化(日本語/英語)
resources/lang/ja/validation.php
と resources/lang/en/validation.php
の両方に、
共通ルール文言と属性名を定義します。アプリの locale
に応じて自動出力