mcpField
L
liquid-theme-a11y

liquid-theme-a11y

github.com/benjaminsehlデザイン・UI1212.8k 回インストール15 週間前に更新
リポジトリ

liquid-theme-a11y について

Shopify Liquid テーマ開発者向けのアクセシビリティリファレンスです。セマンティックな構造、キーボード操作とフォーカス、コンポーネント、動きの設定、コントラストを解説します。

主な機能

  1. 01セマンティック HTML と ARIA の選択指針
  2. 02キーボード操作とフォーカス管理のパターン
  3. 03一般的なコンポーネントのアクセシビリティパターン
  4. 04動きを抑える設定とモバイル向け指針
  5. 05テキスト、UI、フォーカスのコントラスト基準

活用シーン

  1. 01インタラクティブな部品のキーボード操作の確認
  2. 02アクセシブルなダイアログ、フォーム、フィルターの実装
  3. 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

情報を伝える際に色のみに頼らないでください。必ずテキスト、アイコン、またはパターンを併用します。

参考資料

コメント