Liquid Theme Standards

Shopify/liquid-skills/plugins/liquid-skills/skills/liquid-theme-standards

作者 Shopifyae3e4cc3f454923e388bbd841fd931f0c7bf5be4無授權條款35 個星標收錄於 2026年10月9日更新於 2026年10月9日儲存庫6 個月前更新

CSS, JavaScript, and HTML coding standards for Shopify Liquid themes. Covers BEM naming inside stylesheet tags, design tokens, CSS custom properties, Web Components for themes, defensive CSS, and progressive enhancement. Use when writing CSS/JS/HTML in .liquid files or theme asset files.

AI 產生的概覽

針對 Shopify Liquid 主題的 CSS、JavaScript 與 HTML 撰寫規範,涵蓋 BEM、設計權杖與 Web Components。

功能
此技能提供在 Shopify Liquid 主題檔案與主題資源中撰寫 CSS、JavaScript 與 HTML 的撰寫規範與慣例。內容涵蓋 BEM 類別命名、設計權杖與 CSS 自訂屬性、變數作用域、優先級限制、防禦性 CSS、邏輯屬性、Web Component 模式、漸進增強以及原生 HTML 元素。它也說明如何使用 jq 編輯主題 JSON 範本與設定檔。它產出的是指引與程式碼慣例,而非產生的檔案。
適用情境
在 .liquid 檔案或 Shopify 主題資源檔中撰寫或審查 CSS、JavaScript 或 HTML 時使用。適用於需要一致命名、權杖使用與漸進增強的主題開發工作。
執行需求
不需要指令碼或外部套件,僅為說明性內容。它引用兩份可供模型閱讀的參考檔案,並假定處於 Shopify Liquid 主題環境,同時透過 bash 工具使用 jq 編輯 JSON 範本與設定檔。

CSS, JS & HTML Standards for Shopify Liquid Themes

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

CSS in Liquid Themes

Where CSS Lives

LocationLiquid?Use For
{% stylesheet %}NoComponent-scoped styles (one per file)
{% style %}YesDynamic values needing Liquid (e.g., color settings)
assets/*.cssNoShared/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 %}

BEM Naming Convention

.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>

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

CSS Nesting

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

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 Variable Scoping

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;  ">

CSS Property Order

  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

Logical Properties (RTL Support)

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;

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

Modern CSS Features

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

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

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

JavaScript in Liquid Themes

Where JS Lives

LocationLiquid?Use For
{% javascript %}NoComponent-specific scripts (one per file)
assets/*.jsNoShared utilities, Web Components

Web Component Pattern

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 Rules

RuleDoDon't
Loopsfor (const item of items)items.forEach()
Asyncasync/await.then() chains
Variablesconst by defaultlet unless reassigning
ConditionalsEarly returnsNested if/else
URLsnew URL() + URLSearchParamsString concatenation
DependenciesNative browser APIsExternal libraries
Private methods#methodName()_methodName()
TypesJSDoc @typedef, @param, @returnsUntyped

AbortController for 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();  }}

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

HTML Standards

Native Elements First

NeedUseNot
Expandable<details>/<summary>Custom accordion with JS
Dialog/modal<dialog>Custom overlay div
Tooltip/popuppopover attributeCustom positioned div
Search form<search><div class="search">
Form results<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 %}

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

JSON Template & Config Files

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.

Common patterns

bash
# Add a section to a templatejq '.sections.new_section = {"type": "hero", "settings": {"heading": "Welcome"}}' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
# Update a setting valuejq '.current.sections.header.settings.logo_width = 200' config/settings_data.json > /tmp/out && mv /tmp/out config/settings_data.json
# Reorder sectionsjq '.order += ["new_section"]' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
# Remove a sectionjq 'del(.sections.old_banner) | .order -= ["old_banner"]' templates/index.json > /tmp/out && mv /tmp/out templates/index.json
# Read a nested valuejq '.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.

References

  • CSS patterns and examples [blocked]
  • JavaScript patterns and examples [blocked]

來源與署名

來源:Shopify/liquid-skills位於plugins/liquid-skills/skills/liquid-theme-standards提交ae3e4cc

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架