🎨liquid-theme-standards
- プラグイン
- liquid-skills
- ソース
- GitHub で見る ↗
説明
CSSとJavaScript、HTMLに関する、Shopify Liquidテーマ向けコーディング規約。 スタイルシートタグ内でのBEM命名規則、デザイントークン、CSSカスタムプロパティ、テーマ向けWebコンポーネント、防御的CSS、およびプログレッシブエンハンスメントをカバーします。 次のような場合に使用: `.liquid`ファイルやテーマアセットファイルにCSS・JS・HTMLを記述する際。
原文を表示
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.
ユースケース
- ✓.liquidファイルにCSS・JS・HTMLを記述するとき
- ✓テーマアセットファイルを編集するとき
- ✓BEM命名規則に従ってコーディングするとき
- ✓Shopifyテーマのコーディング規約を確認したいとき
本文(日本語訳)
Shopify Liquid テーマの CSS・JS・HTML 標準
基本原則
- プログレッシブエンハンスメント — セマンティック HTML を最優先とし、次に CSS、最後に JS
- 外部依存なし — JavaScript はネイティブブラウザ API のみ使用
- デザイントークン — 色・スペーシング・フォントを直接ハードコードしない
- BEM 命名 — クラス命名を統一して一貫性を保つ
- ディフェンシブ CSS — エッジケースを適切に処理する
Liquid テーマにおける CSS
CSS の配置場所
| 場所 | Liquid 使用可否 | 用途 |
|---|---|---|
{% stylesheet %} |
不可 | コンポーネントスコープのスタイル(ファイルごとに1つ) |
{% style %} |
可 | Liquid が必要な動的な値(カラー設定など) |
assets/*.css |
不可 | 共有・グローバルスタイル |
重要: {% 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 %}
BEM 命名規則
.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 スコープを開始する
<!-- 推奨: 単一の要素レベル -->
<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)
- 可能な限り
0 1 0(シングルクラス)を目標とする - 複雑な親子関係でも最大
0 4 0まで - ID をセレクタとして使用しない(厳守)
!importantを使用しない(厳守)(どうしても必要な場合はコメントで理由を記載)- 要素セレクタを避け、クラスを使用する
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); }
}
/* 非推奨: 第1レベルを超えたネスト */
.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-16ではなく--space-sm - グローバルトークンは
:rootで定義し、スコープ付きトークンはコンポーネントルートで定義する
CSS 変数のスコープ
グローバル — テーマ全体の値は :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;
"
>
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 サポート)
/* 推奨: 論理プロパティ */
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;
ディフェンシブ CSS
.component {
overflow-wrap: break-word; /* テキストのはみ出しを防止 */
min-width: 0; /* フレックスアイテムの縮小を許可 */
max-width: 100%; /* 画像・メディアのサイズを制限 */
isolation: isolate; /* スタッキングコンテキストを生成 */
}
.image-container {
aspect-ratio: 4 / 3; /* レイアウトシフトを防止 */
background: var(--color-surface); /* 画像が欠けた場合のフォールバック */
}
モダン 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); }
パフォーマンス
- アニメーションは
transformとopacityのみに限定する(レイアウトプロパティは対象外) will-changeは必要最小限に使用し、アニメーション終了後は削除する- 独立したレンダリングには
contain: contentを使用する - モバイルでは
vhの代わりにdvhを使用する
モーション軽減への対応
@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 %} |
不可 | コンポーネント固有のスクリプト(ファイルごとに1つ) |
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() {
// イベントリスナーや AbortController のクリーンアップ
}
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
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 |
| ツールチップ・ポップア |
原文(English)を表示
CSS, JS & HTML Standards for Shopify Liquid Themes
Core Principles
- Progressive enhancement — semantic HTML first, CSS second, JS third
- No external dependencies — native browser APIs only for JavaScript
- Design tokens — never hardcode colors, spacing, or fonts
- BEM naming — consistent class naming throughout
- Defensive CSS — handle edge cases gracefully
CSS in Liquid Themes
Where CSS Lives
| 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 %}
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", neverclass="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>
Specificity
- Target
0 1 0(single class) wherever possible - Maximum
0 4 0for 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
/* 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):
: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
remfor spacing and typography (respects user font size preferences) - Name tokens semantically:
--space-smnot--space-16 - Define in
:rootfor global tokens, on component root for scoped tokens
CSS Variable Scoping
Global — 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;
"
>
CSS Property Order
- Layout —
position,display,flex-direction,grid-template-columns - Box model —
width,margin,padding,border - Typography —
font-family,font-size,line-height,color - Visual —
background,opacity,border-radius - Animation —
transition,animation
Logical Properties (RTL Support)
/* 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
.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
/* 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
transformandopacity(never layout properties) - Use
will-changesparingly — remove after animation - Use
contain: contentfor isolated rendering - Use
dvhinstead ofvhon mobile
Reduced Motion
@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
| Location | Liquid? | Use For |
|---|---|---|
{% javascript %} |
No | Component-specific scripts (one per file) |
assets/*.js |
No | Shared utilities, Web Components |
Web Component Pattern
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 Rules
| Rule | Do | Don't |
|---|---|---|
| Loops | for (const item of items) |
items.forEach() |
| Async | async/await |
.then() chains |
| Variables | const by default |
let unless reassigning |
| 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 |
AbortController for 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();
}
}
Component Communication
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
}));
HTML Standards
Native Elements First
| 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> |
<span class="result"> |
Progressive Enhancement
{%- 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
{{ 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
widthandheightto prevent layout shift - Descriptive
alttext; emptyalt=""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
# 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
Prefer jq over edit for any .json file modification — it validates structure, handles escaping, and avoids whitespace/formatting issues.
References
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。