liquid-theme-standards

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CSS, JS & HTML Standards for Shopify Liquid Themes

Shopify Liquid主题的CSS、JS与HTML编码标准

Core Principles

核心原则

  1. Progressive enhancement — semantic HTML first, CSS second, JS third
  2. No external dependencies — native browser APIs only for JavaScript
  3. Design tokens — never hardcode colors, spacing, or fonts
  4. BEM naming — consistent class naming throughout
  5. Defensive CSS — handle edge cases gracefully
  1. 渐进式增强 — 优先使用语义化HTML,其次是CSS,最后是JS
  2. 无外部依赖 — JavaScript仅使用原生浏览器API
  3. 设计令牌 — 绝不硬编码颜色、间距或字体
  4. BEM命名 — 全程保持一致的类命名规则
  5. 防御性CSS — 优雅处理边缘情况

CSS in Liquid Themes

Liquid主题中的CSS

Where CSS Lives

CSS的存放位置

LocationLiquid?Use For
{% stylesheet %}
NoComponent-scoped styles (one per file)
{% style %}
YesDynamic values needing Liquid (e.g., color settings)
assets/*.css
NoShared/global styles
Critical:
{% stylesheet %}
does NOT process Liquid. Use inline
style
attributes for dynamic values:
liquid
{%- 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?适用场景
{% stylesheet %}
组件作用域样式(每个文件对应一个组件)
{% style %}
需要Liquid的动态值(例如颜色设置)
assets/*.css
共享/全局样式
重要提示:
{% stylesheet %}
不处理Liquid代码。如需动态值,请使用内联
style
属性:
liquid
{%- 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--large
Rules:
  • Hyphens separate words:
    .product-card
    , not
    .productCard
  • Single element level only:
    .block__element
    , never
    .block__el1__el2
  • Modifier always paired with base class:
    class="btn btn--primary"
    , never
    class="btn--primary"
    alone
  • 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
    0 1 0
    (single class) wherever possible
  • Maximum
    0 4 0
    for complex parent-child cases
  • Never use IDs as selectors
  • Never use
    !important
    (comment why if absolutely forced to)
  • 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
    rem
    for spacing and typography (respects user font size preferences)
  • Name tokens semantically:
    --space-sm
    not
    --space-16
  • Define in
    :root
    for global tokens, on component root for scoped tokens
所有值均使用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
:root
for theme-wide values Component-scoped — on component root, namespaced:
css
/* 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;
  "
>
全局作用域 — 在
:root
中定义主题级全局值 组件作用域 — 在组件根节点上定义,并添加命名空间:
css
/* 推荐:添加命名空间 */
.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属性顺序

  1. Layout
    position
    ,
    display
    ,
    flex-direction
    ,
    grid-template-columns
  2. Box model
    width
    ,
    margin
    ,
    padding
    ,
    border
  3. Typography
    font-family
    ,
    font-size
    ,
    line-height
    ,
    color
  4. Visual
    background
    ,
    opacity
    ,
    border-radius
  5. Animation
    transition
    ,
    animation
  1. 布局
    position
    ,
    display
    ,
    flex-direction
    ,
    grid-template-columns
  2. 盒模型
    width
    ,
    margin
    ,
    padding
    ,
    border
  3. 排版
    font-family
    ,
    font-size
    ,
    line-height
    ,
    color
  4. 视觉效果
    background
    ,
    opacity
    ,
    border-radius
  5. 动画
    transition
    ,
    animation

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
    transform
    and
    opacity
    (never layout properties)
  • Use
    will-change
    sparingly — remove after animation
  • Use
    contain: content
    for isolated rendering
  • Use
    dvh
    instead of
    vh
    on mobile
  • 仅对
    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的存放位置

LocationLiquid?Use For
{% javascript %}
NoComponent-specific scripts (one per file)
assets/*.js
NoShared utilities, Web Components
位置支持Liquid?适用场景
{% javascript %}
组件专属脚本(每个文件对应一个组件)
assets/*.js
共享工具函数、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规则

RuleDoDon't
Loops
for (const item of items)
items.forEach()
Async
async
/
await
.then()
chains
Variables
const
by default
let
unless reassigning
ConditionalsEarly returnsNested
if/else
URLs
new URL()
+
URLSearchParams
String concatenation
DependenciesNative browser APIsExternal libraries
Private methods
#methodName()
_methodName()
TypesJSDoc
@typedef
,
@param
,
@returns
Untyped
规则推荐不推荐
循环
for (const item of items)
items.forEach()
异步处理
async
/
await
.then()
链式调用
变量声明优先使用
const
除非需要重新赋值,否则不使用
let
条件判断提前返回嵌套
if/else
URL处理
new URL()
+
URLSearchParams
字符串拼接
依赖管理原生浏览器API外部库
私有方法
#methodName()
_methodName()
类型标注JSDoc
@typedef
,
@param
,
@returns
无类型标注

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

优先使用原生元素

NeedUseNot
Expandable
<details>/<summary>
Custom accordion with JS
Dialog/modal
<dialog>
Custom overlay div
Tooltip/popup
popover
attribute
Custom positioned div
Search form
<search>
<div class="search">
Form results
<output>
<span class="result">
需求推荐使用不推荐使用
可展开内容
<details>/<summary>
基于JS的自定义折叠面板
对话框/模态框
<dialog>
自定义覆盖层div
提示框/弹出层
popover
属性
自定义定位div
搜索表单
<search>
<div class="search">
表单结果
<output>
<span class="result">

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
}}
  • loading="lazy"
    on all below-fold images
  • Always set
    width
    and
    height
    to prevent layout shift
  • Descriptive
    alt
    text; empty
    alt=""
    for decorative images
liquid
{{ image | image_url: width: 800 | image_tag:
  loading: 'lazy',
  alt: image.alt | escape,
  width: image.width,
  height: image.height
}}
  • 所有首屏以下的图片添加
    loading="lazy"
  • 始终设置
    width
    height
    以防止布局偏移
  • 描述性的
    alt
    文本;装饰性图片使用空
    alt=""

JSON Template & Config Files

JSON模板与配置文件

Theme templates (
templates/*.json
), section groups (
sections/*.json
), and config files (
config/settings_data.json
) are all JSON. Use
jq
via the
bash
tool to make surgical edits — it's safer and more reliable than string-based find-and-replace for structured data.
主题模板(
templates/*.json
)、板块组(
sections/*.json
)和配置文件(
config/settings_data.json
)均为JSON格式。推荐通过
bash
工具使用
jq
进行精准编辑 — 相比基于字符串的查找替换,它更安全可靠。

Common patterns

常见用法

bash
undefined
bash
undefined

Add 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模式与示例