claude-skills/

Anthropic公式スキル・プラグインの日本語ディレクトリ

last sync 22h ago
スキルOfficialdevelopment

🎨liquid-theme-standards

プラグイン
liquid-skills

説明

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 標準

基本原則

  1. プログレッシブエンハンスメント — セマンティック HTML を最優先とし、次に CSS、最後に JS
  2. 外部依存なし — JavaScript はネイティブブラウザ API のみ使用
  3. デザイントークン — 色・スペーシング・フォントを直接ハードコードしない
  4. BEM 命名 — クラス命名を統一して一貫性を保つ
  5. ディフェンシブ 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 プロパティの記述順

  1. レイアウトpositiondisplayflex-directiongrid-template-columns
  2. ボックスモデルwidthmarginpaddingborder
  3. タイポグラフィfont-familyfont-sizeline-heightcolor
  4. ビジュアルbackgroundopacityborder-radius
  5. アニメーションtransitionanimation

論理プロパティ(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); }

パフォーマンス

  • アニメーションは transformopacity のみに限定する(レイアウトプロパティは対象外)
  • 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

  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

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

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

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

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

  1. Layoutposition, display, flex-direction, grid-template-columns
  2. Box modelwidth, margin, padding, border
  3. Typographyfont-family, font-size, line-height, color
  4. Visualbackground, opacity, border-radius
  5. Animationtransition, 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 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

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

# 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 による自動翻訳です。