liquid-theme-a11y
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAccessibility for Shopify Liquid Themes
Shopify Liquid主题无障碍设计指南
Core Principle
核心原则
Every interactive component must work with keyboard only, screen readers, and reduced-motion preferences. Start with semantic HTML — add ARIA only when native semantics are insufficient.
所有交互式组件必须支持纯键盘操作、屏幕阅读器,并且适配减少动画的偏好设置。优先使用语义化HTML——仅当原生语义不足时才添加ARIA属性。
Decision Table: Which Pattern?
决策表:选择哪种模式?
| Component | HTML Element | ARIA Pattern | Reference |
|---|---|---|---|
| Expandable content | | None needed | Accordion |
| Modal/dialog | | | Modal |
| Tooltip/popup | | | Tooltip |
| Dropdown menu | | | Navigation |
| Tab interface | | | Tabs |
| Carousel/slider | | | Carousel |
| Product card | | | Product card |
| Form | | | Forms |
| Cart drawer | | Focus trap | Cart drawer |
| Price display | | | Prices |
| Filters | | | Filters |
| 组件 | HTML元素 | ARIA模式 | 参考 |
|---|---|---|---|
| 可展开内容 | | 无需额外设置 | 折叠面板 |
| 模态框/对话框 | | | 模态框 |
| 提示框/弹出层 | | | 提示框 |
| 下拉菜单 | | 触发器添加 | 导航 |
| 标签页界面 | | | 标签页 |
| 轮播图/滑块 | | | 轮播图 |
| 产品卡片 | | | 产品卡片 |
| 表单 | | | 表单 |
| 购物车抽屉 | | 焦点捕获 | 购物车抽屉 |
| 价格展示 | | | 价格展示 |
| 筛选器 | | 折叠面板添加 | 产品筛选器 |
Page Structure
页面结构
Landmarks
地标区域
html
<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">
<!-- All page content inside main -->
</main>
<footer role="contentinfo">
<nav aria-label="{{ 'accessibility.footer_navigation' | t }}">...</nav>
</footer>
</body>- Single ,
<header>,<main>per page<footer> - Multiple elements must have distinct
<nav>aria-label - All content must live inside a landmark
html
<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标签内 -->
</main>
<footer role="contentinfo">
<nav aria-label="{{ 'accessibility.footer_navigation' | t }}">...</nav>
</footer>
</body>- 每页仅包含一个、
<header>、<main><footer> - 多个元素必须设置不同的
<nav>aria-label - 所有内容必须包含在某个地标区域内
Skip Link
跳转链接
css
.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);
}css
.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);
}Headings
标题
- One per page, never skip levels (h1 → h3)
<h1> - Use real heading elements, not styled divs
- Template: is typically the page/product title
<h1>
- 每页仅一个,不要跳过标题层级(如从h1直接跳到h3)
<h1> - 使用真实的标题元素,而非样式化的div
- 模板规范:通常为页面/产品标题
<h1>
Focus Management
焦点管理
Focus Indicators
焦点指示器
css
/* 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;
}
}- Minimum 3:1 contrast ratio for focus indicators
- Use (not
:focus-visible) to avoid showing on click:focus - Never without a visible replacement
outline: none
css
/* 所有交互式元素 */
:focus-visible {
outline: 2px solid rgb(var(--color-focus));
outline-offset: 2px;
}
/* 高对比度模式 */
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid LinkText;
}
}- 焦点指示器的对比度至少达到3:1
- 使用(而非
:focus-visible)避免点击时显示:focus - 禁止使用,除非有可见的替代样式
outline: none
Focus Trapping (Modals/Drawers)
焦点捕获(模态框/抽屉)
- Trap focus inside modals, drawers, and dialogs
- Return focus to trigger element on close
- First focusable element gets focus on open
- Query all focusable elements:
a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])
See focus and keyboard patterns for full FocusTrap implementation.
- 将焦点捕获在模态框、抽屉和对话框内
- 关闭时将焦点返回至触发元素
- 打开时自动聚焦第一个可交互元素
- 查询所有可聚焦元素:
a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])
完整的FocusTrap实现请参考焦点与键盘模式。
Component Patterns
组件模式
Product Card
产品卡片
html
<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>Rules:
- Single tab stop per card (the main link)
- on mouse-only shortcuts (quick add)
tabindex="-1" - on
aria-labelledbypointing to the title<article> - Descriptive alt text on images; empty if decorative
alt=""
html
<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>规则:
- 每张卡片仅保留一个制表位(主链接)
- 纯鼠标操作的快捷按钮(如快速加入)设置
tabindex="-1" - 标签使用
<article>指向标题aria-labelledby - 图片添加描述性alt文本;装饰性图片设置
alt=""
Carousel
轮播图
html
<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>Rules:
- Auto-rotation minimum 5 seconds, pause on hover/focus
- Play/pause button required for auto-rotating carousels
- on slide container (set to
aria-live="polite"during auto-rotation)"off" - on inactive slides
aria-hidden="true" - Each slide: +
role="group"aria-roledescription="slide"
html
<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"
Modal
模态框
html
<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">
<!-- Content -->
</div>
</dialog>Rules:
- Use native element
<dialog> - pointing to the title
aria-labelledby - Close on Escape key (native with )
<dialog> - Focus first interactive element on open
- Return focus to trigger on close
html
<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 - 支持Esc键关闭(原生支持)
<dialog> - 打开时自动聚焦第一个可交互元素
- 关闭时将焦点返回至触发元素
Cart Drawer
购物车抽屉
Same as modal pattern but with additional:
- Live region for cart count updates:
<span aria-live="polite" aria-atomic="true"> - Clear "remove item" buttons with
aria-label="{{ 'cart.remove_item' | t: title: item.title }}" - Quantity inputs with associated labels
与模态框模式相同,额外添加:
- 购物车数量更新的实时区域:
<span aria-live="polite" aria-atomic="true"> - 清晰的“移除商品”按钮,设置
aria-label="{{ 'cart.remove_item' | t: title: item.title }}" - 数量输入框关联对应的标签
Forms
表单
html
<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>Rules:
- Every input has a visible with matching
<label>/forid - Use for radio/checkbox groups
<fieldset>/<legend> - Error messages: +
role="alert"linking to inputaria-describedby - on invalid inputs
aria-invalid="true" - attributes on common fields
autocomplete - Required fields: +
required+ visual indicatoraria-required="true"
html
<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"
Product Filters
产品筛选器
html
<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>html
<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>Price Display
价格展示
html
{% 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 %}- Use to provide full price context (sale vs. original)
aria-label - on the visual strikethrough to avoid duplicate reading
aria-hidden="true"
html
{% 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 %}- 使用补充完整价格上下文(促销价vs原价)
aria-label - 视觉删除线设置避免重复朗读
aria-hidden="true"
Accordion
折叠面板
html
<details>
<summary>{{ block.settings.heading }}</summary>
<div class="accordion__content">
{{ block.settings.content }}
</div>
</details>Native provides keyboard and screen reader support automatically.
<details>/<summary>html
<details>
<summary>{{ block.settings.heading }}</summary>
<div class="accordion__content">
{{ block.settings.content }}
</div>
</details>原生自动支持键盘和屏幕阅读器。
<details>/<summary>Tabs
标签页
html
<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 %}- Arrow keys navigate between tabs (left/right)
- Only active tab has , others
tabindex="0"-1
html
<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
Dropdown Navigation
下拉导航
html
<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>html
<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>Tooltip
提示框
html
<button aria-describedby="Tooltip-{{ block.id }}">
{{ 'labels.info' | t }}
</button>
<div id="Tooltip-{{ block.id }}" role="tooltip" popover>
{{ block.settings.tooltip_text }}
</div>html
<button aria-describedby="Tooltip-{{ block.id }}">
{{ 'labels.info' | t }}
</button>
<div id="Tooltip-{{ block.id }}" role="tooltip" popover>
{{ block.settings.tooltip_text }}
</div>Mobile Accessibility
移动端无障碍设计
- Touch targets: minimum 44x44px, 8px spacing between targets
- No orientation lock: never restrict to portrait/landscape
- No hover-only content: everything accessible via tap
- Use instead of
dvhfor mobile viewport unitsvh
- 触摸目标: 最小尺寸44x44px,目标间间距至少8px
- 禁止锁定方向: 不要限制为仅竖屏或横屏
- 无悬停专属内容: 所有内容均可通过点击访问
- 移动端视口单位使用替代
dvhvh
Animation & Motion
动画与运动
css
/* 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;
}
}- No flashing above 3 times per second
- Auto-playing animations need pause/stop controls
- Meaningful animations only — don't animate for decoration
css
/* 始终提供减少动画的选项 */
@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 Utility
视觉隐藏工具类
css
.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;
}Use for screen-reader-only content like labels and descriptions.
css
.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;
}用于仅屏幕阅读器可见的内容,如标签和描述。
Color Contrast
颜色对比度
| Element | Minimum Ratio |
|---|---|
| Normal text (<18px / <14px bold) | 4.5:1 |
| Large text (≥18px / ≥14px bold) | 3:1 |
| UI components & graphics | 3:1 |
| Focus indicators | 3:1 |
Never rely solely on color to convey information — always pair with text, icons, or patterns.
| 元素 | 最低对比度 |
|---|---|
| 普通文本(<18px / <14px粗体) | 4.5:1 |
| 大文本(≥18px / ≥14px粗体) | 3:1 |
| UI组件与图形 | 3:1 |
| 焦点指示器 | 3:1 |
切勿仅依赖颜色传递信息——始终搭配文本、图标或图案。
References
参考资料
- Component accessibility patterns
- Focus and keyboard patterns
- 组件无障碍模式
- 焦点与键盘模式