关于 liquid-theme-standards
为 Shopify Liquid 主题制定 CSS、JavaScript 和 HTML 编写规范的参考技能,面向主题开发者。内容涵盖统一编码约定、渐进增强,以及安全修改 JSON 主题文件的方法。
主要功能
- 01CSS 组织与 BEM 命名
- 02设计令牌与防御式样式
- 03原生浏览器 API 和 Web Components
- 04语义化 HTML 与渐进增强
- 05图像无障碍与布局稳定性
- 06使用 jq 修改 JSON 模板
适用场景
- 01组织 Liquid 主题中的 CSS
- 02用原生 JavaScript 构建主题组件
- 03完善 RTL 与减少动态效果支持
- 04安全编辑 Shopify JSON 模板
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ benjaminsehl/ liquid-skills --skill liquid-theme-standards安装后在 Claude Code 中输入/liquid-theme-standards确认已加载。
附带文件references/css-patterns.mdreferences/javascript-patterns.md
详细介绍
Shopify Liquid 主题的 CSS、JS 和 HTML 标准
核心原则
- 渐进增强——先使用语义化 HTML,其次是 CSS,最后是 JS
- 无外部依赖——JavaScript 仅使用浏览器原生 API
- 设计令牌——绝不硬编码颜色、间距或字体
- BEM 命名——始终使用一致的类命名方式
- 防御性 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 属性顺序
- 布局——
position、display、flex-direction、grid-template-columns - 盒模型——
width、margin、padding、border - 排版——
font-family、font-size、line-height、color - 视觉效果——
background、opacity、border-radius - 动画——
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 |
| URL | new 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