Laravelの画面で同じボタン、カード、入力欄を繰り返し記述しているなら、Bladeコンポーネントに切り出すと修正箇所をまとめられます。ただし、見た目だけを共通化しても、リンクとボタンの使い分け、エラー表示、キーボード操作が画面ごとに崩れては保守しやすいUIになりません。
このガイドでは、匿名Bladeコンポーネントの構成から、Props、Slot、属性バッグ、フォーム、配色、モーダルまでを一つの流れで説明します。Laravelの基本的なビュー作成を経験し、次に再利用性とアクセシビリティを整えたい開発者を想定しています。
この記事で扱う内容
- 匿名Bladeコンポーネントの配置と呼び出し方
- Props、Slot、属性バッグの役割分担
- ボタン、カード、入力欄の実装例
- 文字、境界線、フォーカス表示のコントラスト確認
- モーダルに必要なARIAとキーボード操作
- 共通部品をチームで保守するための確認手順
Bladeコンポーネントが解決する重複
Bladeコンポーネントは、ビューの一部を名前付きの部品として切り出し、必要な値や内容を渡して再利用する仕組みです。たとえば、送信ボタンの余白やフォーカス表示を一つのコンポーネントで管理すれば、仕様変更時に同じ修正を複数画面へ繰り返す必要が減ります。
| 対象 | コンポーネントにまとめる内容 | 利用側で渡す内容 |
|---|---|---|
| ボタン | 要素の種類、余白、配色、フォーカス表示 | ラベル、表示形式、追加属性 |
| カード | 枠、余白、見出しと本文の配置 | 見出し、本文、フッター |
| 入力欄 | ラベル、必須表示、エラーとの関連付け | ID、項目名、値、入力形式 |
共通化の目的は、すべての差を消すことではありません。変えてよい値を明示し、それ以外の構造を揃えることです。部品の責任範囲が明確なら、利用側のビューも読みやすくなります。
匿名コンポーネントの配置
匿名コンポーネントは、専用のPHPクラスを持たず、Bladeファイルだけで定義するコンポーネントです。アプリケーション内の小さなUI部品から始める場合は、次のように resources/views/components 配下へ用途別に配置できます。
resources/
└── views/
└── components/
├── button.blade.php
├── card.blade.php
└── form/
└── input.blade.php
button.blade.php は <x-button>、form/input.blade.php は <x-form.input> として呼び出せます。階層を増やすときは、ファイルの見た目ではなく役割で分けます。
components/:複数機能で使うボタンやカードcomponents/form/:ラベル、入力欄、エラー表示を扱う部品components/layout/:ページ全体の枠組みを担う部品
Props、Slot、属性バッグの役割
Propsは利用側から受け取る値を定義する
Propsは、コンポーネントが受け取る値と初期値を宣言するために使います。@props自体が型を保証するわけではないため、許可する値を限定したい場合は、コンポーネント内で候補を定義して想定外の値を既定値へ戻します。
{{-- resources/views/components/button.blade.php --}}
@props([
'type' => 'button',
'variant' => 'primary',
])
@php
$baseClass = 'inline-flex items-center rounded px-4 py-2 focus:outline-none focus:ring-2';
$variants = [
'primary' => 'bg-blue-600 text-white hover:bg-blue-700',
'secondary' => 'bg-gray-200 text-gray-800 hover:bg-gray-300',
];
$variantClass = $variants[$variant] ?? $variants['primary'];
@endphp
<button
type='{{ $type }}'
{{ $attributes->merge(['class' => $baseClass . ' ' . $variantClass]) }}
>
{{ $slot }}
</button>
この例では、HTMLのボタン種別を表す type と、見た目を切り替える variant を分けています。利用側は次のように記述します。
<x-button variant='secondary' class='mt-4'>
下書きを保存
</x-button>
画面遷移には <a href='...'>、送信やモーダルの開閉などの操作には <button> を使います。見た目が同じでも役割が異なるため、リンク用とボタン用は別のコンポーネントにすると誤用を防ぎやすくなります。
Slotはまとまりのある内容を差し替える
Slotは、利用側に記述した内容をコンポーネントへ渡す仕組みです。既定の $slot に加えて、見出しやフッターのような名前付きSlotを用意できます。
{{-- resources/views/components/card.blade.php --}}
<section {{ $attributes->merge(['class' => 'rounded-lg border p-4']) }}>
@isset($header)
<div class='mb-3 border-b pb-2'>{{ $header }}</div>
@endisset
<div>{{ $slot }}</div>
@isset($footer)
<div class='mt-3 border-t pt-2'>{{ $footer }}</div>
@endisset
</section>
<x-card>
<x-slot:header>アカウント設定</x-slot>
<p>通知先と表示名を変更できます。</p>
<x-slot:footer>
<x-button>保存</x-button>
</x-slot>
</x-card>
短い文字列はProps、複数要素を含む本文や見出しはSlotにすると、コンポーネントの呼び出し側から構造を把握しやすくなります。
属性バッグは追加属性を引き継ぐ
属性バッグは、Propsとして受け取らなかったHTML属性をまとめた $attributes です。mergeを使うと、コンポーネントの既定クラスを残しながら、利用側の class、id、data-*、ARIA属性などを引き継げます。属性を受け入れる要素を一つに決めておくと、利用側の指定先が曖昧になりません。
エラーと必須項目を関連付ける入力コンポーネント
フォーム入力では、見た目の共通化に加えて、ラベルと入力欄、入力欄とエラーメッセージを関連付けます。idの重複は関連付けを壊すため、利用側が画面内で一意の値を渡します。
{{-- resources/views/components/form/input.blade.php --}}
@props([
'id',
'label',
'name' => null,
'type' => 'text',
'value' => null,
'required' => false,
])
@php($fieldName = $name ?? $id)
<div class='mb-4'>
<label for='{{ $id }}' class='mb-1 block text-gray-800'>
{{ $label }}
@if ($required)
<span aria-hidden='true' class='text-red-700'>*</span>
<span class='sr-only'>(必須)</span>
@endif
</label>
<input
id='{{ $id }}'
type='{{ $type }}'
name='{{ $fieldName }}'
value='{{ old($fieldName, $value) }}'
@if ($required) required @endif
@error($fieldName)
aria-invalid='true'
aria-describedby='{{ $id }}-error'
@enderror
{{ $attributes->merge(['class' => 'w-full rounded border px-3 py-2']) }}
>
@error($fieldName)
<p id='{{ $id }}-error' class='mt-1 text-red-700'>
{{ $message }}
</p>
@enderror
</div>
aria-describedbyは、参照先のエラーがあるときだけ出力します。必須状態はまずHTMLの required で表し、視覚的な印だけに頼らず「必須」というテキストも用意します。エラー時の色、枠線、文言をコンポーネントで揃えると、画面ごとの対応漏れを減らせます。
配色は前景色と背景色の組み合わせで確認する
コントラスト比は、単独のHEX値ではなく、文字や境界線の色と、その隣にある背景色の組み合わせで決まります。元の配色表のように色名だけへ比率を固定すると、背景や表示状態が変わったときに誤った判定になります。
WCAG 2.2の達成基準を実務で読む方法も確認しながら、次の組み合わせを状態ごとに検証します。
| 対象 | 基準 | 確認する状態 |
|---|---|---|
| 通常サイズの文字 | 背景に対して4.5:1以上 | 通常、ホバー、無効時を除く各状態 |
| 大きな文字 | 背景に対して3:1以上 | 通常、ホバー、選択時 |
| 操作部品を識別する境界線や状態表示 | 隣接色に対して3:1以上 | 通常、フォーカス、選択時 |
ボタンでは文字とボタン背景だけでなく、フォーカスリングと周囲の背景も確認します。エラー表示は赤色だけに頼らず、文言やアイコンなど別の手掛かりも組み合わせます。
ダークモードを用意する場合は、ライトテーマの合格結果を流用せず、各テーマで再計測します。prefers-color-schemeはARIA属性ではなくCSSのメディア機能であり、利用者またはページの配色設定に合わせてスタイルを切り替えるために使います。
モーダルはARIAと動作を一組で実装する
モーダルのコンテナには、用途に応じて role='dialog'、aria-modal='true'、見出しを参照する aria-labelledby を指定します。
<div
role='dialog'
aria-modal='true'
aria-labelledby='account-dialog-title'
>
<h2 id='account-dialog-title'>アカウント設定</h2>
{{ $slot }}
<button type='button' aria-label='設定画面を閉じる'>閉じる</button>
</div>
これらの属性は、モーダルの意味を支援技術へ伝えるためのものです。属性を付けるだけでは、背景の操作制限やフォーカス移動は実装されません。JavaScriptを含む開閉処理では、次の動作をまとめて確認します。
- 開いた直後に、内容に応じた要素へフォーカスを移す
- TabキーとShift+Tabキーでフォーカスをモーダル内に保つ
- Escapeキーと画面上の閉じるボタンで閉じられるようにする
- 表示中は背景を操作できない状態にする
- 閉じた後は、原則としてモーダルを開いた要素へフォーカスを戻す
実装例と確認項目は、アクセシブルなモーダルダイアログ設計ガイドで詳しく整理しています。
共通コンポーネントを保守する単位
コンポーネントを増やす前に、各部品の責任範囲を決めます。ボタンにURL、モーダル制御、送信処理まで詰め込むと、Propsの組み合わせが増え、どの画面で何が起きるかを追いにくくなります。
- 一つのコンポーネントは一つの役割に絞る
- 見た目の種類は、用途が説明できる名前で限定する
- Propsの初期値と許可する値を明示する
- Slotへ渡してよい内容と、固定する構造を決める
- キーボード操作、エラー、フォーカス表示を部品単位で確認する
ボタンやフォームを個別に揃えた後、命名、配色、状態、利用ルールまでチームで共有する段階では、Bladeコンポーネントをデザインシステムへ発展させる方法が次の検討材料になります。
既存画面へ導入する手順
- 複数画面で重複しているボタン、カード、入力欄を一つ選ぶ
- 固定する構造と、PropsやSlotで変える内容を分ける
- 単体のコンポーネントを作り、利用側の属性が正しく引き継がれるか確認する
- キーボード操作、エラー表示、各表示状態のコントラストを確認する
- 既存画面を一つずつ置き換え、差異が必要なら理由を記録する
最初からすべてのUIをライブラリ化する必要はありません。変更頻度が高く、重複による修正漏れが起きやすい部品から始めると、共通化の範囲を判断しやすくなります。
公開前のチェックリスト
- 本文内にH1を置かず、見出しレベルが順番どおりになっている
- リンクとボタンを目的に応じて使い分けている
- Props、Slot、属性バッグの役割が重複していない
- ラベルの
forと入力欄の idが一致している - エラーがあるときだけ
aria-describedbyの参照先を出力している - 文字、境界線、フォーカス表示を実際の背景色との組み合わせで確認した
- モーダルをキーボードだけで開閉し、フォーカスを追える
- ライトテーマとダークテーマを別々に確認した
まとめ
Bladeコンポーネントの再利用性は、ファイルを分けるだけでは得られません。固定する構造、利用側へ許可する変更、HTMLの意味、エラーやフォーカスの状態を一つの部品として設計することで、修正しやすく操作しやすいUIになります。まずは重複の多い一部品を選び、実装と確認手順をセットで共通化してください。
この記事に関連する株式会社greedenの取り組み
Bladeコンポーネントにアクセシビリティ要件を組み込めば、画面追加のたびに配慮が抜けるリスクを抑えられます。株式会社greedenは、Webアクセシビリティの知見を生かし、誰もが使いやすいWeb環境づくりを支援しています。
