liquid-theme-standards について
Shopify Liquidテーマ向けに、CSS、JavaScript、HTMLの実装規約をまとめたリファレンススキルです。テーマ開発者が一貫した規約やプログレッシブエンハンスメントを適用し、JSONファイルを安全に編集する際に役立ちます。
主な機能
- 01CSS構成とBEM命名
- 02デザイントークンと防御的スタイル
- 03ネイティブブラウザーAPIとWeb Components
- 04セマンティックHTMLとプログレッシブエンハンスメント
- 05画像のアクセシビリティとレイアウト安定性
- 06jqによるJSONテンプレート編集
活用シーン
- 01LiquidテーマのCSS構成
- 02ネイティブJavaScriptでテーマコンポーネントを作成
- 03RTLと動きを減らす設定への対応
- 04Shopify 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 %} | いいえ | コンポーネントにスコープされたスタイル(ファイルごとに 1 つ) |
{% 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は使用しません - 要素の階層は 1 つだけにします:
.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-16ではなく--space-sm - グローバルトークンは
: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を使用します - モバイルでは
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 コンポーネント |
Web コンポーネントのパターン
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 | 型のないコード |
Fetch での 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> | JavaScript を使ったカスタムアコーディオン |
| ダイアログ/モーダル | <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 です。構造化データを編集する場合、文字列ベースの検索・置換より安全で信頼性の高い jq を、bash ツール経由で使用して慎重に編集してください。
よくあるパターン
# 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 ファイルを変更する場合は、edit より jq を優先してください。構造を検証し、エスケープを処理して、空白や書式設定に関する問題を回避できます。