liquid-theme-standards
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCSS, JS & HTML Standards for Shopify Liquid Themes
Shopify Liquid主题的CSS、JS与HTML编码标准
Core Principles
核心原则
- Progressive enhancement — semantic HTML first, CSS second, JS third
- No external dependencies — native browser APIs only for JavaScript
- Design tokens — never hardcode colors, spacing, or fonts
- BEM naming — consistent class naming throughout
- Defensive CSS — handle edge cases gracefully
- 渐进式增强 — 优先使用语义化HTML,其次是CSS,最后是JS
- 无外部依赖 — JavaScript仅使用原生浏览器API
- 设计令牌 — 绝不硬编码颜色、间距或字体
- BEM命名 — 全程保持一致的类命名规则
- 防御性CSS — 优雅处理边缘情况
CSS in Liquid Themes
Liquid主题中的CSS
Where CSS Lives
CSS的存放位置
| Location | Liquid? | Use For |
|---|---|---|
| No | Component-scoped styles (one per file) |
| Yes | Dynamic values needing Liquid (e.g., color settings) |
| No | Shared/global styles |
Critical: does NOT process Liquid. Use inline attributes for dynamic values:
{% stylesheet %}styleliquid
{%- comment -%} Do: inline variables {%- endcomment -%}
<div
class="hero"
style="--bg-color: {{ section.settings.bg_color }}; --padding: {{ section.settings.padding }}px;"
>
{%- comment -%} Don't: Liquid inside stylesheet {%- endcomment -%}
{% stylesheet %}
.hero { background: {{ section.settings.bg_color }}; } /* Won't work */
{% endstylesheet %}| 位置 | 支持Liquid? | 适用场景 |
|---|---|---|
| 否 | 组件作用域样式(每个文件对应一个组件) |
| 是 | 需要Liquid的动态值(例如颜色设置) |
| 否 | 共享/全局样式 |
重要提示: 不处理Liquid代码。如需动态值,请使用内联属性:
{% stylesheet %}styleliquid
{%- comment -%} 推荐:内联变量 {%- endcomment -%}
<div
class="hero"
style="--bg-color: {{ section.settings.bg_color }}; --padding: {{ section.settings.padding }}px;"
>
{%- comment -%} 不推荐:在stylesheet标签内使用Liquid {%- endcomment -%}
{% stylesheet %}
.hero { background: {{ section.settings.bg_color }}; } /* 无法生效 */
{% endstylesheet %}BEM Naming Convention
BEM命名规范
.block → Component root: .product-card
.block__element → Child: .product-card__title
.block--modifier → Variant: .product-card--featured
.block__element--modifier → Element variant: .product-card__title--largeRules:
- Hyphens separate words: , not
.product-card.productCard - Single element level only: , never
.block__element.block__el1__el2 - Modifier always paired with base class: , never
class="btn btn--primary"aloneclass="btn--primary" - Start new BEM scope when a child could be standalone
html
<!-- Good: single element level -->
<div class="product-card">
<h3 class="product-card__title">{{ product.title }}</h3>
<span class="product-card__button-label">{{ 'add_to_cart' | t }}</span>
</div>
<!-- Good: new BEM scope for standalone component -->
<div class="product-card">
<button class="button button--primary">
<span class="button__label">{{ 'add_to_cart' | t }}</span>
</button>
</div>.block → 组件根节点:.product-card
.block__element → 子元素:.product-card__title
.block--modifier → 变体:.product-card--featured
.block__element--modifier → 子元素变体:.product-card__title--large规则:
- 使用连字符分隔单词:,而非
.product-card.productCard - 仅支持单层元素级别:,禁止使用
.block__element.block__el1__el2 - 修饰符必须与基础类配合使用:,禁止单独使用
class="btn btn--primary"class="btn--primary" - 当子元素可独立使用时,开启新的BEM作用域
html
<!-- 规范:单层元素级别 -->
<div class="product-card">
<h3 class="product-card__title">{{ product.title }}</h3>
<span class="product-card__button-label">{{ 'add_to_cart' | t }}</span>
</div>
<!-- 规范:为独立组件开启新的BEM作用域 -->
<div class="product-card">
<button class="button button--primary">
<span class="button__label">{{ 'add_to_cart' | t }}</span>
</button>
</div>Specificity
选择器优先级
- Target (single class) wherever possible
0 1 0 - Maximum for complex parent-child cases
0 4 0 - Never use IDs as selectors
- Never use (comment why if absolutely forced to)
!important - Avoid element selectors — use classes
- 尽可能使用优先级(单个类选择器)
0 1 0 - 复杂父子场景下最大优先级为
0 4 0 - 禁止使用ID作为选择器
- 禁止使用(若必须使用,请添加注释说明原因)
!important - 避免使用元素选择器 — 优先使用类选择器
CSS Nesting
CSS嵌套
css
/* Do: media queries inside selectors */
.header {
width: 100%;
@media screen and (min-width: 750px) {
width: auto;
}
}
/* Do: state modifiers with & */
.button {
background: var(--color-primary);
&:hover { background: var(--color-primary-hover); }
&:focus-visible { outline: 2px solid var(--color-focus); }
&[disabled] { opacity: 0.5; }
}
/* Do: parent modifier affecting children (single level) */
.card--featured {
.card__title { font-size: var(--font-size-xl); }
}
/* Don't: nested beyond first level */
.parent {
.child {
.grandchild { } /* Too deep */
}
}css
/* 推荐:媒体查询嵌套在选择器内部 */
.header {
width: 100%;
@media screen and (min-width: 750px) {
width: auto;
}
}
/* 推荐:使用&表示状态修饰符 */
.button {
background: var(--color-primary);
&:hover { background: var(--color-primary-hover); }
&:focus-visible { outline: 2px solid var(--color-focus); }
&[disabled] { opacity: 0.5; }
}
/* 推荐:父组件修饰符影响子元素(仅单层嵌套) */
.card--featured {
.card__title { font-size: var(--font-size-xl); }
}
/* 不推荐:嵌套超过一层 */
.parent {
.child {
.grandchild { } /* 嵌套过深 */
}
}Design Tokens
设计令牌
Use CSS custom properties for all values — never hardcode colors, spacing, or fonts. Define a consistent scale and reference it everywhere.
Example scale (adapt to your theme's needs):
css
:root {
/* Spacing — use a consistent scale */
--space-2xs: 0.5rem; --space-xs: 0.75rem; --space-sm: 1rem;
--space-md: 1.5rem; --space-lg: 2rem; --space-xl: 3rem;
/* Typography — relative units */
--font-size-sm: 0.875rem; --font-size-base: 1rem;
--font-size-lg: 1.125rem; --font-size-xl: 1.25rem; --font-size-2xl: 1.5rem;
}Key principles:
- Use for spacing and typography (respects user font size preferences)
rem - Name tokens semantically: not
--space-sm--space-16 - Define in for global tokens, on component root for scoped tokens
:root
所有值均使用CSS自定义属性 — 绝不硬编码颜色、间距或字体。定义统一的比例规范,并在所有地方引用。
示例比例(可根据主题需求调整):
css
:root {
/* 间距 — 使用统一比例 */
--space-2xs: 0.5rem; --space-xs: 0.75rem; --space-sm: 1rem;
--space-md: 1.5rem; --space-lg: 2rem; --space-xl: 3rem;
/* 排版 — 使用相对单位 */
--font-size-sm: 0.875rem; --font-size-base: 1rem;
--font-size-lg: 1.125rem; --font-size-xl: 1.25rem; --font-size-2xl: 1.5rem;
}核心原则:
- 间距和排版使用单位(尊重用户的字体大小偏好)
rem - 令牌名称语义化:使用而非
--space-sm--space-16 - 全局令牌定义在中,组件作用域令牌定义在组件根节点上
:root
CSS Variable Scoping
CSS变量作用域
Global — in for theme-wide values
Component-scoped — on component root, namespaced:
:rootcss
/* Do: namespaced */
.facets {
--facets-padding: var(--space-md);
--facets-z-index: 3;
}
/* Don't: generic names that collide */
.facets {
--padding: var(--space-md);
--z-index: 3;
}Override via inline style for section/block settings:
liquid
<section
class="hero"
style="
--hero-bg: {{ section.settings.bg_color }};
--hero-padding: {{ section.settings.padding }}px;
"
>全局作用域 — 在中定义主题级全局值
组件作用域 — 在组件根节点上定义,并添加命名空间:
:rootcss
/* 推荐:添加命名空间 */
.facets {
--facets-padding: var(--space-md);
--facets-z-index: 3;
}
/* 不推荐:使用易冲突的通用名称 */
.facets {
--padding: var(--space-md);
--z-index: 3;
}通过内联style覆盖区块/板块设置:
liquid
<section
class="hero"
style="
--hero-bg: {{ section.settings.bg_color }};
--hero-padding: {{ section.settings.padding }}px;
"
>CSS Property Order
CSS属性顺序
- Layout — ,
position,display,flex-directiongrid-template-columns - Box model — ,
width,margin,paddingborder - Typography — ,
font-family,font-size,line-heightcolor - Visual — ,
background,opacityborder-radius - Animation — ,
transitionanimation
- 布局 — ,
position,display,flex-directiongrid-template-columns - 盒模型 — ,
width,margin,paddingborder - 排版 — ,
font-family,font-size,line-heightcolor - 视觉效果 — ,
background,opacityborder-radius - 动画 — ,
transitionanimation
Logical Properties (RTL Support)
逻辑属性(RTL支持)
css
/* Do: logical properties */
padding-inline: 2rem;
padding-block: 1rem;
margin-inline: auto;
border-inline-end: 1px solid var(--color-border);
text-align: start;
inset: 0;
/* Don't: physical properties */
padding-left: 2rem;
text-align: left;
top: 0; right: 0; bottom: 0; left: 0;css
/* 推荐:使用逻辑属性 */
padding-inline: 2rem;
padding-block: 1rem;
margin-inline: auto;
border-inline-end: 1px solid var(--color-border);
text-align: start;
inset: 0;
/* 不推荐:使用物理属性 */
padding-left: 2rem;
text-align: left;
top: 0; right: 0; bottom: 0; left: 0;Defensive CSS
防御性CSS
css
.component {
overflow-wrap: break-word; /* Prevent text overflow */
min-width: 0; /* Allow flex items to shrink */
max-width: 100%; /* Constrain images/media */
isolation: isolate; /* Create stacking context */
}
.image-container {
aspect-ratio: 4 / 3; /* Prevent layout shift */
background: var(--color-surface); /* Fallback for missing images */
}css
.component {
overflow-wrap: break-word; /* 防止文本溢出 */
min-width: 0; /* 允许弹性项目收缩 */
max-width: 100%; /* 限制图片/媒体元素宽度 */
isolation: isolate; /* 创建层叠上下文 */
}
.image-container {
aspect-ratio: 4 / 3; /* 防止布局偏移 */
background: var(--color-surface); /* 图片加载失败时的回退背景 */
}Modern CSS Features
现代CSS特性
css
/* Container queries for responsive components */
.product-grid { container-type: inline-size; }
@container (min-width: 400px) {
.product-card { grid-template-columns: 1fr 1fr; }
}
/* Fluid spacing */
.section { padding: clamp(1rem, 4vw, 3rem); }
/* Intrinsic sizing */
.content { width: min(100%, 800px); }css
/* 容器查询实现响应式组件 */
.product-grid { container-type: inline-size; }
@container (min-width: 400px) {
.product-card { grid-template-columns: 1fr 1fr; }
}
/* 流体间距 */
.section { padding: clamp(1rem, 4vw, 3rem); }
/* 固有尺寸 */
.content { width: min(100%, 800px); }Performance
性能优化
- Animate only and
transform(never layout properties)opacity - Use sparingly — remove after animation
will-change - Use for isolated rendering
contain: content - Use instead of
dvhon mobilevh
- 仅对和
transform执行动画(禁止对布局属性执行动画)opacity - 谨慎使用— 动画结束后移除该属性
will-change - 对独立渲染的元素使用
contain: content - 移动端使用替代
dvh单位vh
Reduced Motion
减少动画
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;
}
}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;
}
}JavaScript in Liquid Themes
Liquid主题中的JavaScript
Where JS Lives
JS的存放位置
| Location | Liquid? | Use For |
|---|---|---|
| No | Component-specific scripts (one per file) |
| No | Shared utilities, Web Components |
| 位置 | 支持Liquid? | 适用场景 |
|---|---|---|
| 否 | 组件专属脚本(每个文件对应一个组件) |
| 否 | 共享工具函数、Web Components |
Web Component Pattern
Web Component模式
javascript
class ProductCard extends HTMLElement {
connectedCallback() {
this.button = this.querySelector('[data-add-to-cart]');
this.button?.addEventListener('click', this.#handleClick.bind(this));
}
disconnectedCallback() {
// Clean up event listeners, abort controllers
}
async #handleClick(event) {
event.preventDefault();
this.button.disabled = true;
try {
const formData = new FormData();
formData.append('id', this.dataset.variantId);
formData.append('quantity', '1');
const response = await fetch('/cart/add.js', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed');
this.dispatchEvent(new CustomEvent('cart:item-added', {
detail: await response.json(),
bubbles: true
}));
} catch (error) {
console.error('Add to cart error:', error);
} finally {
this.button.disabled = false;
}
}
}
customElements.define('product-card', ProductCard);liquid
<product-card data-variant-id="{{ product.selected_or_first_available_variant.id }}">
<button data-add-to-cart>{{ 'products.add_to_cart' | t }}</button>
</product-card>javascript
class ProductCard extends HTMLElement {
connectedCallback() {
this.button = this.querySelector('[data-add-to-cart]');
this.button?.addEventListener('click', this.#handleClick.bind(this));
}
disconnectedCallback() {
// 清理事件监听器、中止控制器
}
async #handleClick(event) {
event.preventDefault();
this.button.disabled = true;
try {
const formData = new FormData();
formData.append('id', this.dataset.variantId);
formData.append('quantity', '1');
const response = await fetch('/cart/add.js', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed');
this.dispatchEvent(new CustomEvent('cart:item-added', {
detail: await response.json(),
bubbles: true
}));
} catch (error) {
console.error('Add to cart error:', error);
} finally {
this.button.disabled = false;
}
}
}
customElements.define('product-card', ProductCard);liquid
<product-card data-variant-id="{{ product.selected_or_first_available_variant.id }}">
<button data-add-to-cart>{{ 'products.add_to_cart' | t }}</button>
</product-card>JavaScript Rules
JavaScript规则
| Rule | Do | Don't |
|---|---|---|
| Loops | | |
| Async | | |
| Variables | | |
| Conditionals | Early returns | Nested |
| URLs | | String concatenation |
| Dependencies | Native browser APIs | External libraries |
| Private methods | | |
| Types | JSDoc | Untyped |
| 规则 | 推荐 | 不推荐 |
|---|---|---|
| 循环 | | |
| 异步处理 | | |
| 变量声明 | 优先使用 | 除非需要重新赋值,否则不使用 |
| 条件判断 | 提前返回 | 嵌套 |
| URL处理 | | 字符串拼接 |
| 依赖管理 | 原生浏览器API | 外部库 |
| 私有方法 | | |
| 类型标注 | JSDoc | 无类型标注 |
AbortController for Fetch
使用AbortController管理Fetch请求
javascript
class DataLoader extends HTMLElement {
#controller = null;
async load(url) {
this.#controller?.abort();
this.#controller = new AbortController();
try {
const response = await fetch(url, { signal: this.#controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (error.name !== 'AbortError') throw error;
return null;
}
}
disconnectedCallback() {
this.#controller?.abort();
}
}javascript
class DataLoader extends HTMLElement {
#controller = null;
async load(url) {
this.#controller?.abort();
this.#controller = new AbortController();
try {
const response = await fetch(url, { signal: this.#controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (error.name !== 'AbortError') throw error;
return null;
}
}
disconnectedCallback() {
this.#controller?.abort();
}
}Component Communication
组件通信
Parent → Child: Call public methods
javascript
this.querySelector('child-component')?.publicMethod(data);Child → Parent: Dispatch custom events
javascript
this.dispatchEvent(new CustomEvent('child:action', {
detail: { value },
bubbles: true
}));父组件 → 子组件: 调用公共方法
javascript
this.querySelector('child-component')?.publicMethod(data);子组件 → 父组件: 派发自定义事件
javascript
this.dispatchEvent(new CustomEvent('child:action', {
detail: { value },
bubbles: true
}));HTML Standards
HTML标准
Native Elements First
优先使用原生元素
| Need | Use | Not |
|---|---|---|
| Expandable | | Custom accordion with JS |
| Dialog/modal | | Custom overlay div |
| Tooltip/popup | | Custom positioned div |
| Search form | | |
| Form results | | |
| 需求 | 推荐使用 | 不推荐使用 |
|---|---|---|
| 可展开内容 | | 基于JS的自定义折叠面板 |
| 对话框/模态框 | | 自定义覆盖层div |
| 提示框/弹出层 | | 自定义定位div |
| 搜索表单 | | |
| 表单结果 | | |
Progressive Enhancement
渐进式增强
liquid
{%- comment -%} Works without JS {%- endcomment -%}
<details class="accordion">
<summary>{{ block.settings.heading }}</summary>
<div class="accordion__content">
{{ block.settings.content }}
</div>
</details>
{%- comment -%} Enhanced with JS {%- endcomment -%}
{% javascript %}
// Optional: smooth animation, analytics tracking
{% endjavascript %}liquid
{%- comment -%} 无JS时也能正常工作 {%- endcomment -%}
<details class="accordion">
<summary>{{ block.settings.heading }}</summary>
<div class="accordion__content">
{{ block.settings.content }}
</div>
</details>
{%- comment -%} 使用JS增强体验 {%- endcomment -%}
{% javascript %}
// 可选:平滑动画、埋点统计
{% endjavascript %}Images
图片处理
liquid
{{ image | image_url: width: 800 | image_tag:
loading: 'lazy',
alt: image.alt | escape,
width: image.width,
height: image.height
}}- on all below-fold images
loading="lazy" - Always set and
widthto prevent layout shiftheight - Descriptive text; empty
altfor decorative imagesalt=""
liquid
{{ image | image_url: width: 800 | image_tag:
loading: 'lazy',
alt: image.alt | escape,
width: image.width,
height: image.height
}}- 所有首屏以下的图片添加
loading="lazy" - 始终设置和
width以防止布局偏移height - 描述性的文本;装饰性图片使用空
altalt=""
JSON Template & Config Files
JSON模板与配置文件
Theme templates (), section groups (), and config files () are all JSON. Use via the tool to make surgical edits — it's safer and more reliable than string-based find-and-replace for structured data.
templates/*.jsonsections/*.jsonconfig/settings_data.jsonjqbash主题模板()、板块组()和配置文件()均为JSON格式。推荐通过工具使用进行精准编辑 — 相比基于字符串的查找替换,它更安全可靠。
templates/*.jsonsections/*.jsonconfig/settings_data.jsonbashjqCommon patterns
常见用法
bash
undefinedbash
undefinedAdd a section to a template
向模板中添加板块
jq '.sections.new_section = {"type": "hero", "settings": {"heading": "Welcome"}}' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
jq '.sections.new_section = {"type": "hero", "settings": {"heading": "Welcome"}}' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
Update a setting value
更新配置值
jq '.current.sections.header.settings.logo_width = 200' config/settings_data.json > /tmp/out && mv /tmp/out config/settings_data.json
jq '.current.sections.header.settings.logo_width = 200' config/settings_data.json > /tmp/out && mv /tmp/out config/settings_data.json
Reorder sections
调整板块顺序
jq '.order += ["new_section"]' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
jq '.order += ["new_section"]' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
Remove a section
删除板块
jq 'del(.sections.old_banner) | .order -= ["old_banner"]' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
jq 'del(.sections.old_banner) | .order -= ["old_banner"]' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
Read a nested value
读取嵌套值
jq '.sections.header.settings' templates/index.json
**Prefer `jq` over `edit`** for any `.json` file modification — it validates structure, handles escaping, and avoids whitespace/formatting issues.jq '.sections.header.settings' templates/index.json
**优先使用`jq`而非`edit`**修改`.json`文件 — 它会验证结构、处理转义,并避免空格/格式问题。References
参考资料
- CSS patterns and examples
- JavaScript patterns and examples
- CSS模式与示例
- JavaScript模式与示例