liquid-theme-a11y について
Shopify Liquid テーマ開発者向けのアクセシビリティリファレンスです。セマンティックな構造、キーボード操作とフォーカス、コンポーネント、動きの設定、コントラストを解説します。
主な機能
- 01セマンティック HTML と ARIA の選択指針
- 02キーボード操作とフォーカス管理のパターン
- 03一般的なコンポーネントのアクセシビリティパターン
- 04動きを抑える設定とモバイル向け指針
- 05テキスト、UI、フォーカスのコントラスト基準
活用シーン
- 01インタラクティブな部品のキーボード操作の確認
- 02アクセシブルなダイアログ、フォーム、フィルターの実装
- 03動きと色のコントラスト要件の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ benjaminsehl/ liquid-skills --skill liquid-theme-a11yインストール後、Claude Code で/liquid-theme-a11yを入力して読み込まれたか確認します。
同梱ファイルreferences/component-patterns.mdreferences/focus-and-keyboard.md
詳細
Shopify Liquid テーマのアクセシビリティ
基本原則
すべてのインタラクティブコンポーネントは、キーボードのみで操作でき、スクリーンリーダーに対応し、モーション低減の設定に従う必要があります。まずセマンティックな HTML を使用し、ネイティブのセマンティクスだけでは不十分な場合にのみ ARIA を追加します。
選択表:どのパターンを使いますか?
| コンポーネント | HTML 要素 | ARIA パターン | リファレンス |
|---|---|---|---|
| 展開可能なコンテンツ | <details>/<summary> | 不要 | アコーディオン |
| モーダル/ダイアログ | <dialog> | aria-modal="true" | モーダル |
| ツールチップ/ポップアップ | [popover] 属性 | フォールバックとして role="tooltip" | ツールチップ |
| ドロップダウンメニュー | <nav> + <ul> | トリガーに aria-expanded | ナビゲーション |
| タブインターフェース | <div> | role="tablist/tab/tabpanel" | タブ |
| カルーセル/スライダー | <div> | role="region" + aria-roledescription | カルーセル |
| 商品カード | <article> | aria-labelledby | 商品カード |
| フォーム | <form> | aria-invalid、aria-describedby | フォーム |
| カートドロワー | <dialog> | フォーカストラップ | カートドロワー |
| 価格表示 | <span> | コンテキストを示す aria-label | 価格 |
| フィルター | <form> + <fieldset> | 開閉式要素に aria-expanded | フィルター |
ページ構造
ランドマーク
<body>
<a href="#main-content" class="skip-link">{{ 'accessibility.skip_to_content' | t }}</a>
<header role="banner">
<nav aria-label="{{ 'accessibility.main_navigation' | t }}">...</nav>
</header>
<main id="main-content">
</main>
<footer role="contentinfo">
<nav aria-label="{{ 'accessibility.footer_navigation' | t }}">...</nav>
</footer>
</body>
- 1 ページにつき
<header>、<main>、<footer>はそれぞれ 1 つにします - 複数の
<nav>要素には、それぞれ異なるaria-labelを設定する必要があります - すべてのコンテンツはランドマークの内側に配置する必要があります
スキップリンク
.skip-link {
position: absolute;
inset-inline-start: -999px;
z-index: 999;
}
.skip-link:focus {
position: fixed;
inset-block-start: 0;
inset-inline-start: 0;
padding: 1rem;
background: var(--color-background);
color: var(--color-foreground);
}
見出し
- 1 ページにつき
<h1>は 1 つにし、レベルを飛ばさないでください(h1 → h3) - スタイルを設定した div ではなく、実際の見出し要素を使用してください
- テンプレート:通常、
<h1>はページまたは商品のタイトルです
フォーカス管理
フォーカスインジケーター
/* All interactive elements */
:focus-visible {
outline: 2px solid rgb(var(--color-focus));
outline-offset: 2px;
}
/* High contrast mode */
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid LinkText;
}
}
- フォーカスインジケーターのコントラスト比は最低 3:1 にします
- クリック時に表示されないよう、
:focusではなく:focus-visibleを使用します - 視認できる代替表示を用意せずに
outline: noneを指定してはいけません
フォーカストラップ(モーダル/ドロワー)
- モーダル、ドロワー、ダイアログの内部にフォーカスを閉じ込めます
- 閉じたときはトリガー要素にフォーカスを戻します
- 開いたときは最初のフォーカス可能な要素にフォーカスします
- フォーカス可能な要素をすべて取得します:
a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])
FocusTrap の完全な実装については、フォーカスとキーボード操作のパターンを参照してください。
コンポーネントのパターン
商品カード
<article class="product-card" aria-labelledby="ProductTitle-{{ product.id }}">
<a href="{{ product.url }}" class="product-card__link" aria-labelledby="ProductTitle-{{ product.id }}">
<img
src="{{ product.featured_image | image_url: width: 400 }}"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
width="{{ product.featured_image.width }}"
height="{{ product.featured_image.height }}"
>
</a>
<h3 id="ProductTitle-{{ product.id }}">
<a href="{{ product.url }}">{{ product.title }}</a>
</h3>
<div class="product-card__price" aria-label="{{ 'products.price_label' | t: price: product.price | money }}">
{{ product.price | money }}
</div>
<button
class="product-card__quick-add"
tabindex="-1"
aria-label="{{ 'products.quick_add' | t: title: product.title }}"
>
{{ 'products.add_to_cart' | t }}
</button>
</article>
ルール:
- カードごとのタブストップは 1 つ(メインリンク)にします
- マウスのみで使うショートカット(クイック追加)には
tabindex="-1"を設定します <article>のaria-labelledbyでタイトルを参照します- 画像には説明的な代替テキストを設定し、装飾目的の場合は
alt=""にします
カルーセル
<div
role="region"
aria-roledescription="carousel"
aria-label="{{ section.settings.heading | escape }}"
>
<div class="carousel__controls">
<button
aria-label="{{ 'accessibility.previous_slide' | t }}"
aria-controls="CarouselSlides-{{ section.id }}"
>{% render 'icon-chevron-left' %}</button>
<button
aria-label="{{ 'accessibility.next_slide' | t }}"
aria-controls="CarouselSlides-{{ section.id }}"
>{% render 'icon-chevron-right' %}</button>
<button
aria-label="{{ 'accessibility.pause_slideshow' | t }}"
aria-pressed="false"
>{% render 'icon-pause' %}</button>
</div>
<div id="CarouselSlides-{{ section.id }}" aria-live="polite">
{% for slide in section.blocks %}
<div
role="group"
aria-roledescription="slide"
aria-label="{{ 'accessibility.slide_n_of_total' | t: n: forloop.index, total: forloop.length }}"
{% unless forloop.first %}aria-hidden="true"{% endunless %}
>
{{ slide.settings.content }}
</div>
{% endfor %}
</div>
</div>
ルール:
- 自動切り替えの間隔は最低 5 秒にし、ホバー時/フォーカス時は一時停止します
- 自動切り替えを行うカルーセルには、再生/一時停止ボタンが必要です
- スライドコンテナに
aria-live="polite"を設定します(自動切り替え中は"off"にします) - 非アクティブなスライドには
aria-hidden="true"を設定します - 各スライドには
role="group"+aria-roledescription="slide"を設定します
モーダル
<dialog
id="Modal-{{ section.id }}"
aria-labelledby="ModalTitle-{{ section.id }}"
aria-modal="true"
>
<div class="modal__header">
<h2 id="ModalTitle-{{ section.id }}">{{ title }}</h2>
<button
type="button"
aria-label="{{ 'accessibility.close' | t }}"
on:click="/closeModal"
>{% render 'icon-close' %}</button>
</div>
<div class="modal__content">
</div>
</dialog>
ルール:
- ネイティブの
<dialog>要素を使用します aria-labelledbyでタイトルを参照します- Escape キーで閉じるようにします(
<dialog>のネイティブ機能を使用) - 開いたときは最初のインタラクティブ要素にフォーカスします
- 閉じたときはトリガーにフォーカスを戻します
カートドロワー
モーダルのパターンに加えて、以下を適用します:
- カート数の更新を通知するライブリージョン:
<span aria-live="polite" aria-atomic="true"> aria-label="{{ 'cart.remove_item' | t: title: item.title }}"を指定した、明確な「商品を削除」ボタン- 対応するラベルを設定した数量入力欄
フォーム
<form action="{{ routes.cart_url }}" method="post">
<div class="form__field">
<label for="Email-{{ section.id }}">{{ 'forms.email' | t }}</label>
<input
type="email"
id="Email-{{ section.id }}"
name="email"
required
aria-required="true"
autocomplete="email"
aria-describedby="EmailError-{{ section.id }}"
>
<p
id="EmailError-{{ section.id }}"
class="form__error"
role="alert"
hidden
>{{ 'forms.email_required' | t }}</p>
</div>
</form>
ルール:
- すべての入力欄に、対応する
for/idを設定した可視の<label>を用意します - ラジオボタン/チェックボックスのグループには
<fieldset>/<legend>を使用します - エラーメッセージには
role="alert"を設定し、aria-describedbyで入力欄に関連付けます - 無効な入力欄には
aria-invalid="true"を設定します - 一般的な入力欄には
autocomplete属性を設定します - 必須項目には
required+aria-required="true"+ 視覚的な表示を設定します
商品フィルター
<form class="facets">
<div class="facets__group">
<button
type="button"
aria-expanded="false"
aria-controls="FilterColor-{{ section.id }}"
>{{ 'filters.color' | t }}</button>
<fieldset id="FilterColor-{{ section.id }}" hidden>
<legend class="visually-hidden">{{ 'filters.filter_by_color' | t }}</legend>
{% for color in colors %}
<label>
<input type="checkbox" name="filter.color" value="{{ color }}">
{{ color }}
</label>
{% endfor %}
</fieldset>
</div>
<div aria-live="polite" aria-atomic="true">
{{ 'filters.results_count' | t: count: results.size }}
</div>
</form>
価格表示
{% if product.compare_at_price > product.price %}
<div class="price" aria-label="{{ 'products.sale_price_label' | t: sale_price: product.price | money, original_price: product.compare_at_price | money }}">
<s aria-hidden="true">{{ product.compare_at_price | money }}</s>
<span>{{ product.price | money }}</span>
</div>
{% else %}
<div class="price">{{ product.price | money }}</div>
{% endif %}
- セール価格と元の価格の両方を伝えるには、
aria-labelを使用します - 視覚的な取り消し線には
aria-hidden="true"を設定し、重複して読み上げられないようにします
アコーディオン
<details>
<summary>{{ block.settings.heading }}</summary>
<div class="accordion__content">
{{ block.settings.content }}
</div>
</details>
ネイティブの <details>/<summary> は、キーボードとスクリーンリーダーのサポートを自動的に提供します。
タブ
<div role="tablist" aria-label="{{ 'accessibility.product_tabs' | t }}">
{% for tab in tabs %}
<button
role="tab"
id="Tab-{{ tab.id }}"
aria-selected="{% if forloop.first %}true{% else %}false{% endif %}"
aria-controls="Panel-{{ tab.id }}"
tabindex="{% if forloop.first %}0{% else %}-1{% endif %}"
>{{ tab.title }}</button>
{% endfor %}
</div>
{% for tab in tabs %}
<div
role="tabpanel"
id="Panel-{{ tab.id }}"
aria-labelledby="Tab-{{ tab.id }}"
{% unless forloop.first %}hidden{% endunless %}
tabindex="0"
>{{ tab.content }}</div>
{% endfor %}
- 矢印キーでタブ間を移動できます(左右)
- アクティブなタブのみ
tabindex="0"を設定し、その他のタブには-1を設定します
ドロップダウンナビゲーション
<nav aria-label="{{ 'accessibility.main_navigation' | t }}">
<ul role="list">
{% for link in linklists.main-menu.links %}
<li>
{% if link.links.size > 0 %}
<button aria-expanded="false" aria-controls="Submenu-{{ forloop.index }}">
{{ link.title }}
</button>
<ul id="Submenu-{{ forloop.index }}" hidden role="list">
{% for child in link.links %}
<li><a href="{{ child.url }}">{{ child.title }}</a></li>
{% endfor %}
</ul>
{% else %}
<a href="{{ link.url }}">{{ link.title }}</a>
{% endif %}
</li>
{% endfor %}
</ul>
</nav>
ツールチップ
<button aria-describedby="Tooltip-{{ block.id }}">
{{ 'labels.info' | t }}
</button>
<div id="Tooltip-{{ block.id }}" role="tooltip" popover>
{{ block.settings.tooltip_text }}
</div>
モバイルのアクセシビリティ
- タッチターゲット: 最小44x44px、ターゲット間は8px空けます
- 画面の向きを固定しない: 縦向きまたは横向きに制限しないでください
- ホバー時のみ表示されるコンテンツを使わない: すべてタップでアクセスできるようにします
- モバイルのビューポート単位には
vhではなくdvhを使用します
アニメーションと動き
/* Always provide reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
- 1秒あたり3回を超える点滅は禁止です
- 自動再生するアニメーションには、一時停止/停止コントロールが必要です
- 意味のあるアニメーションのみを使用し、装飾目的でアニメーションを使わないでください
視覚的に隠すユーティリティ
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
ラベルや説明など、スクリーンリーダーのみで読み上げるコンテンツに使用します。
色のコントラスト
| 要素 | 最小比率 |
|---|---|
| 通常のテキスト(<18px/<14px 太字) | 4.5:1 |
| 大きなテキスト(≥18px/≥14px 太字) | 3:1 |
| UI コンポーネントとグラフィック | 3:1 |
| フォーカスインジケーター | 3:1 |
情報を伝える際に色のみに頼らないでください。必ずテキスト、アイコン、またはパターンを併用します。
参考資料
コメント
Markdown 対応