liquid-theme-standards by benjaminsehl/liquid-skills
npx skills add https://github.com/benjaminsehl/liquid-skills --skill liquid-theme-standards| 位置 | 支持 Liquid? | 用途 |
|---|---|---|
{% stylesheet %} | 否 | 组件作用域的样式(每个文件一个) |
{% style %} | 是 | 需要 Liquid 处理的动态值(例如,颜色设置) |
assets/*.css |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 否 |
| 共享/全局样式 |
关键点: {% stylesheet %} 不处理 Liquid。对于动态值,请使用内联 style 属性:
{%- 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 %}
.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__el2class="btn btn--primary",永远不要单独使用 class="btn--primary"<!-- 正确:单层元素 -->
<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>
0 1 0(单个类)为目标0 4 0!important(如果绝对被迫使用,请注释说明原因)/* 正确做法:媒体查询放在选择器内部 */
.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 { } /* 太深了 */
}
}
对所有值使用 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 中,作用域令牌定义在组件根元素上全局 — 在 :root 中定义主题范围内的值
组件作用域 — 在组件根元素上定义,并加上命名空间:
/* 正确做法:带命名空间 */
.facets {
--facets-padding: var(--space-md);
--facets-z-index: 3;
}
/* 错误做法:通用名称易冲突 */
.facets {
--padding: var(--space-md);
--z-index: 3;
}
通过内联样式覆盖区块/板块设置:
<section
class="hero"
style="
--hero-bg: {{ section.settings.bg_color }};
--hero-padding: {{ section.settings.padding }}px;
"
>
position, display, flex-direction, grid-template-columnswidth, margin, padding, borderfont-family, font-size, line-height, colorbackground, opacity, border-radiustransition, animation/* 正确做法:逻辑属性 */
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;
.component {
overflow-wrap: break-word; /* 防止文本溢出 */
min-width: 0; /* 允许 flex 项目收缩 */
max-width: 100%; /* 限制图片/媒体大小 */
isolation: isolate; /* 创建堆叠上下文 */
}
.image-container {
aspect-ratio: 4 / 3; /* 防止布局偏移 */
background: var(--color-surface); /* 图片缺失时的后备方案 */
}
/* 用于响应式组件的容器查询 */
.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); }
transform 和 opacity 进行动画处理(切勿对布局属性)will-change — 动画结束后移除contain: contentdvh 而不是 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 %} | 否 | 组件特定脚本(每个文件一个) |
assets/*.js | 否 | 共享工具函数、Web 组件 |
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);
<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>
| 规则 | 正确做法 | 错误做法 |
|---|---|---|
| 循环 | 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 | 无类型 |
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
}));
| 需求 | 使用 | 不使用 |
|---|---|---|
| 可展开 | <details>/<summary> | 使用 JS 的自定义手风琴 |
| 对话框/模态框 | <dialog> | 自定义覆盖层 div |
| 工具提示/弹出框 | popover 属性 | 自定义定位的 div |
| 搜索表单 | <search> | <div class="search"> |
| 表单结果 | <output> | <span class="result"> |
{%- 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 %}
{{ 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=""主题模板 (templates/*.json)、区块组 (sections/*.json) 和配置文件 (config/settings_data.json) 都是 JSON。通过 bash 工具使用 jq 进行精确编辑 — 对于结构化数据,这比基于字符串的查找和替换更安全、更可靠。
# 向模板添加区块
jq '.sections.new_section = {"type": "hero", "settings": {"heading": "Welcome"}}' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
# 更新设置值
jq '.current.sections.header.settings.logo_width = 200' config/settings_data.json > /tmp/out && mv /tmp/out config/settings_data.json
# 重新排序区块
jq '.order += ["new_section"]' 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
# 读取嵌套值
jq '.sections.header.settings' templates/index.json
对于任何 .json 文件修改,优先使用 jq 而不是 edit — 它能验证结构、处理转义并避免空格/格式问题。
每周安装数
376
代码仓库
GitHub 星标数
91
首次出现
2026年2月23日
安全审计
安装于
claude-code296
opencode272
codex271
cursor270
kimi-cli269
gemini-cli269
| Location | Liquid? | Use For |
|---|---|---|
{% stylesheet %} | No | Component-scoped styles (one per file) |
{% style %} | Yes | Dynamic values needing Liquid (e.g., color settings) |
assets/*.css | No | Shared/global styles |
Critical: {% stylesheet %} does NOT process Liquid. Use inline style attributes for dynamic values:
{%- 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 %}
.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
<!-- 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>0 1 0 (single class) wherever possible0 4 0 for complex parent-child cases!important (comment why if absolutely forced to)/* 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 */
}
}
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):
: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:
rem for spacing and typography (respects user font size preferences)--space-sm not --space-16:root for global tokens, on component root for scoped tokensGlobal — in :root for theme-wide values Component-scoped — on component root, namespaced:
/* 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:
<section
class="hero"
style="
--hero-bg: {{ section.settings.bg_color }};
--hero-padding: {{ section.settings.padding }}px;
"
>
position, display, flex-direction, grid-template-columnswidth, margin, padding, borderfont-family, font-size, line-height, /* 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;
.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 */
}
/* 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 and opacity (never layout properties)will-change sparingly — remove after animationcontain: content for isolated renderingdvh instead of vh on mobile@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;
}
}
| Location | Liquid? | Use For |
|---|---|---|
{% javascript %} | No | Component-specific scripts (one per file) |
assets/*.js | No | Shared utilities, Web Components |
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>
| Rule | Do | Don't |
|---|---|---|
| Loops | for (const item of items) | items.forEach() |
| Async | async/await | .then() chains |
| Variables | const by default | let unless reassigning |
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();
}
}
Parent → Child: Call public methods
this.querySelector('child-component')?.publicMethod(data);
Child → Parent: Dispatch custom events
this.dispatchEvent(new CustomEvent('child:action', {
detail: { value },
bubbles: true
}));
| Need | Use | Not |
|---|---|---|
| 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> |
{%- 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" on all below-fold imageswidth and height to prevent layout shiftalt text; empty alt="" for decorative imagesTheme 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.
# 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
Preferjq over edit for any .json file modification — it validates structure, handles escaping, and avoids whitespace/formatting issues.
Weekly Installs
376
Repository
GitHub Stars
91
First Seen
Feb 23, 2026
Security Audits
Gen Agent Trust HubPassSocketFailSnykPass
Installed on
claude-code296
opencode272
codex271
cursor270
kimi-cli269
gemini-cli269
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装
设计增长循环框架:54位产品负责人教你构建病毒式增长引擎
758 周安装
Parallel Deep Research:AI 驱动的深度研究工具,实现自动化详尽分析与报告生成
758 周安装
股票分析工具 - 一键生成NVDA、TSLA、AAPL等股票情感分析报告
759 周安装
spec-miner:AI代码规范挖掘器 - 从代码库自动提取需求与文档
760 周安装
销售线索筛选框架指南:提升转化率,优化销售流程 | 销售效率工具
760 周安装
hairy-utils:JavaScript/TypeScript 核心工具库 | 异步处理、类型检查、函数式编程
761 周安装
colorbackground, opacity, border-radiustransition, animation| Conditionals | Early returns | Nested if/else |
| URLs | new URL() + URLSearchParams | String concatenation |
| Dependencies | Native browser APIs | External libraries |
| Private methods | #methodName() | _methodName() |
| Types | JSDoc @typedef, @param, @returns | Untyped |
<span class="result"> |