关于 liquid-theme-a11y
面向 Shopify Liquid 主题开发者的无障碍参考,提供语义结构、键盘与焦点行为、组件模式、动态效果偏好及对比度方面的指导。
主要功能
- 01语义化 HTML 与 ARIA 选择指南
- 02键盘操作和焦点管理模式
- 03常见组件的无障碍模式
- 04减少动态效果及移动端指导
- 05文本、界面与焦点的对比度要求
适用场景
- 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>
- 每个页面只能有一个
<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