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文本、界面与焦点的对比度要求

适用场景

  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>
  • 每个页面只能有一个 <header>、<main> 和 <footer>
  • 多个 <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);
}
标题
  • 每个页面只能有一个 <h1>,且不得跳过标题级别(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-visible(而不是 :focus),避免点击时显示
  • 如果没有可见的替代样式,绝不要使用 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>

规则:

  • 每张卡片只有一个 Tab 停靠点(主链接)
  • 仅供鼠标使用的快捷操作(快速添加)设置 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>

规则:

  • 每个输入框都必须有可见的 <label>,且其 for/id 相匹配
  • 单选框/复选框组使用 <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
  • 不锁定屏幕方向: 切勿限制为纵向或横向
  • 不依赖悬停显示内容: 所有内容均可通过轻触访问
  • 移动设备视口单位使用 dvh,而不是 vh

动画与动态效果

/* 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;
  }
}
  • 每秒闪烁次数不得超过 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