サイトアイコン IT & ライフハックブログ|学びと実践のためのアイデア集

Laravel Bladeコンポーネント実践ガイド|再利用しやすいUIとアクセシビリティ設計

coding script

Photo by Markus Spiske on Pexels.com

Laravelの画面で同じボタン、カード、入力欄を繰り返し記述しているなら、Bladeコンポーネントに切り出すと修正箇所をまとめられます。ただし、見た目だけを共通化しても、リンクとボタンの使い分け、エラー表示、キーボード操作が画面ごとに崩れては保守しやすいUIになりません。

このガイドでは、匿名Bladeコンポーネントの構成から、Props、Slot、属性バッグ、フォーム、配色、モーダルまでを一つの流れで説明します。Laravelの基本的なビュー作成を経験し、次に再利用性とアクセシビリティを整えたい開発者を想定しています。

この記事で扱う内容

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> ​として呼び出せます。階層を増やすときは、ファイルの見た目ではなく役割で分けます。

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の達成基準を実務で読む方法も確認しながら、次の組み合わせを状態ごとに検証します。

WCAG ​2.2 ​レベルAAで確認する主なコントラスト
対象 基準 確認する状態
通常サイズの文字 背景に対して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を含む開閉処理では、次の動作をまとめて確認します。

実装例と確認項目は、アクセシブルなモーダルダイアログ設計ガイドで詳しく整理しています。

共通コンポーネントを保守する単位

コンポーネントを増やす前に、各部品の責任範囲を決めます。ボタンにURL、モーダル制御、送信処理まで詰め込むと、Propsの組み合わせが増え、どの画面で何が起きるかを追いにくくなります。

ボタンやフォームを個別に揃えた後、命名、配色、状態、利用ルールまでチームで共有する段階では、Bladeコンポーネントをデザインシステムへ発展させる方法が次の検討材料になります。

既存画面へ導入する手順

  1. 複数画面で重複しているボタン、カード、入力欄を一つ選ぶ
  2. 固定する構造と、PropsやSlotで変える内容を分ける
  3. 単体のコンポーネントを作り、利用側の属性が正しく引き継がれるか確認する
  4. キーボード操作、エラー表示、各表示状態のコントラストを確認する
  5. 既存画面を一つずつ置き換え、差異が必要なら理由を記録する

最初からすべてのUIをライブラリ化する必要はありません。変更頻度が高く、重複による修正漏れが起きやすい部品から始めると、共通化の範囲を判断しやすくなります。

公開前のチェックリスト

まとめ

Bladeコンポーネントの再利用性は、ファイルを分けるだけでは得られません。固定する構造、利用側へ許可する変更、HTMLの意味、エラーやフォーカスの状態を一つの部品として設計することで、修正しやすく操作しやすいUIになります。まずは重複の多い一部品を選び、実装と確認手順をセットで共通化してください。

この記事に関連する株式会社greedenの取り組み

Bladeコンポーネントにアクセシビリティ要件を組み込めば、画面追加のたびに配慮が抜けるリスクを抑えられます。株式会社greedenは、Webアクセシビリティの知見を生かし、誰もが使いやすいWeb環境づくりを支援しています。

モバイルバージョンを終了