mcpField
L
liquid-theme-standards

liquid-theme-standards

github.com/shopify开发36244 次安装32 周前更新
代码仓库

关于 liquid-theme-standards

此编码 Skill 为 Shopify Liquid 主题制定 CSS、JavaScript、HTML 和 JSON 编辑规范,涵盖渐进增强、设计令牌、BEM 命名、浏览器原生 API,以及使用 `jq` 编辑结构化 JSON。

主要功能

  1. 01CSS 令牌与 BEM 命名规范
  2. 02渐进增强与防御式 CSS
  3. 03浏览器原生 API 与 Web Components
  4. 04HTML 可访问性与图像规范
  5. 05使用 `jq` 编辑结构化 JSON

适用场景

  1. 01构建 Shopify Liquid 主题组件
  2. 02审查主题 CSS 与 JavaScript
  3. 03维护响应式及支持 RTL 的样式
  4. 04编辑主题 JSON 模板与设置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/shopify/liquid-skills --skill liquid-theme-standards

安装后在 Claude Code 中输入/liquid-theme-standards确认已加载。

附带文件references/css-patterns.mdreferences/javascript-patterns.md

详细介绍

Shopify Liquid 主题的 CSS、JS 和 HTML 标准

核心原则

  1. 渐进增强——先使用语义化 HTML,其次是 CSS,最后是 JS
  2. 无外部依赖——JavaScript 仅使用浏览器原生 API
  3. 设计令牌——绝不硬编码颜色、间距或字体
  4. BEM 命名——始终使用一致的类命名方式
  5. 防御性 CSS——妥善处理边缘情况

Liquid 主题中的 CSS

CSS 的位置
位置Liquid?用途
{% stylesheet %}否组件作用域样式(每个文件一个)
{% style %}是需要使用 Liquid 的动态值(例如颜色设置)
assets/*.css否共享/全局样式

重要:{% stylesheet %} 不会处理 Liquid。动态值请使用内联 style 属性:

{%- 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 %}
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

规则:

  • 使用连字符分隔单词:.product-card,而不是 .productCard
  • 元素只允许单层:.block__element,绝不使用 .block__el1__el2
  • 修饰符始终与基础类配对:class="btn btn--primary",绝不单独使用 class="btn--primary"
  • 子元素可能独立存在时,开始新的 BEM 作用域

<div class="product-card">
  <h3 class="product-card__title">{{ product.title }}</h3>
  <span class="product-card__button-label">{{ 'add_to_cart' | t }}</span>
</div>


<div class="product-card">
  <button class="button button--primary">
    <span class="button__label">{{ 'add_to_cart' | t }}</span>
  </button>
</div>
特异性
  • 尽可能使用 0 1 0(单个类)
  • 复杂的父子关系最多使用 0 4 0
  • 绝不将 ID 用作选择器
  • 绝不使用 !important(若确实不得不使用,请注释说明原因)
  • 避免使用元素选择器,改用类
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 自定义属性,绝不硬编码颜色、间距或字体。定义一致的比例,并在各处引用。

比例示例(根据主题需求调整):

: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;
}

关键原则:

  • 间距和排版使用 rem(遵循用户的字体大小偏好)
  • 使用语义化的令牌名称:--space-sm,而不是 --space-16
  • 全局令牌定义在 :root 中,作用域令牌定义在组件根元素上
CSS 变量作用域

全局——在 :root 中定义主题范围的值 组件作用域——在组件根元素上定义,并使用命名空间:

/* 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;
}

通过内联样式覆盖分区/区块设置:

<section
  class="hero"
  style="
    --hero-bg: {{ section.settings.bg_color }};
    --hero-padding: {{ section.settings.padding }}px;
  "
>
CSS 属性顺序
  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
逻辑属性(支持 RTL)
/* 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
.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 特性
/* 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); }
性能
  • 仅对 transform 和 opacity 设置动画(绝不对布局属性设置动画)
  • 谨慎使用 will-change,动画结束后移除
  • 使用 contain: content 实现隔离渲染
  • 移动设备上使用 dvh 而不是 vh
减少动态效果
@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;
  }
}

Liquid 主题中的 JavaScript

JS 的位置
位置Liquid?用途
{% javascript %}否组件专用脚本(每个文件一个)
assets/*.js否共享工具、Web Components
Web Component 模式
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);
<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 规则
规则应使用不应使用
循环for (const item of items)items.forEach()
异步async/await.then() 链式调用
变量默认使用 const除非需要重新赋值,否则不要使用 let
条件判断提前返回嵌套的 if/else
URLnew URL() + URLSearchParams字符串拼接
依赖浏览器原生 API外部库
私有方法#methodName()_methodName()
类型JSDoc @typedef、@param、@returns未标注类型
使用 AbortController 处理 Fetch
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();
  }
}
组件间通信

**父级 → 子级:**调用公共方法

this.querySelector('child-component')?.publicMethod(data);

**子级 → 父级:**派发自定义事件

this.dispatchEvent(new CustomEvent('child:action', {
  detail: { value },
  bubbles: true
}));

HTML 标准

优先使用原生元素
需求使用不要使用
可展开内容<details>/<summary>使用 JS 的自定义手风琴
对话框/模态框<dialog>自定义覆盖层 div
工具提示/弹出层popover 属性自定义定位 div
搜索表单<search><div class="search">
表单结果<output><span class="result">
渐进增强
{%- 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 %}
图片
{{ 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 模板与配置文件

主题模板(templates/*.json)、分区组(sections/*.json)和配置文件(config/settings_data.json)都是 JSON。使用 bash 工具通过 jq 进行精准修改——对于结构化数据,这比基于字符串的查找和替换更安全、更可靠。

常见模式
# 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

# 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

# Reorder sections
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

# Read a nested value
jq '.sections.header.settings' templates/index.json

修改任何 .json 文件时,优先使用 jq 而非 edit——它可以验证结构、处理转义,并避免空白和格式问题。

参考资料

评论

支持 Markdown