liquid-theme-standards
benjaminsehl/liquid-skills
CSS, JavaScript, and HTML coding standards for Shopify Liquid themes with BEM, design tokens, and progressive enhancement.
What is liquid-theme-standards?
Establishes best practices for writing CSS, JavaScript, and HTML in Shopify Liquid theme files and assets. Covers BEM naming conventions, CSS custom properties, Web Components, defensive CSS techniques, and progressive enhancement principles. Use when building or maintaining .liquid files and theme asset files to ensure consistency and maintainability.
- Apply BEM naming convention for consistent class naming across components
- Use CSS custom properties (design tokens) for colors, spacing, and typography instead of hardcoded values
- Structure CSS in stylesheet tags, style tags, and asset files with clear separation of concerns
- Build Web Components for reusable, encapsulated JavaScript functionality
- Implement defensive CSS techniques to handle edge cases and prevent layout shifts
- Support progressive enhancement with semantic HTML, CSS, and JavaScript layers
How to install liquid-theme-standards
npx skills add https://github.com/benjaminsehl/liquid-skills --skill liquid-theme-standardsHow to use liquid-theme-standards
- 1.Define a consistent design token scale in CSS custom properties (spacing, typography, colors) in :root or component roots
- 2.Use BEM naming convention when writing classes: .block, .block__element, .block--modifier
- 3.Place component-scoped CSS in {% stylesheet %} tags and dynamic styles in inline style attributes with Liquid variables
- 4.Build interactive features as Web Components extending HTMLElement with connectedCallback and disconnectedCallback lifecycle methods
- 5.Apply defensive CSS patterns like overflow-wrap, min-width: 0, and aspect-ratio to prevent layout issues
- 6.Use logical CSS properties (padding-inline, text-align: start) instead of physical properties for RTL support
Use cases
- Styling product cards, headers, and other components with scoped CSS and BEM classes
- Creating dynamic section styles by passing Liquid variables through inline style attributes
- Building reusable Web Components for cart interactions and product selection
- Ensuring responsive layouts with container queries and fluid spacing techniques
- Supporting right-to-left (RTL) languages using logical CSS properties
- Shopify theme developers
- Front-end engineers building custom Liquid themes
- Theme designers implementing design systems
- Developers maintaining existing Shopify theme codebases
liquid-theme-standards FAQ
No. The {% stylesheet %} tag does not process Liquid. Use inline style attributes on HTML elements instead to pass dynamic values like colors and spacing from section settings.
Target 0 1 0 (single class) wherever possible, with a maximum of 0 4 0 for complex parent-child cases. Never use IDs as selectors or !important declarations.
Start a new BEM scope. For example, a button inside a product card should use .button and .button__label classes, not .product-card__button and .product-card__button__label.
Use only native browser APIs. No external dependencies. Build Web Components extending HTMLElement for reusable interactive features, and use fetch for API calls.
Use the aspect-ratio CSS property on image containers and set a background color fallback. This reserves space before the image loads.
Full instructions (SKILL.md)
Source of truth, from benjaminsehl/liquid-skills.
name: liquid-theme-standards description: "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."
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
Related skills
More from benjaminsehl/liquid-skills and the wider catalog.

shopify-liquid-themes
Generate Shopify Liquid theme code with correct schema, headers, and patterns for sections, blocks, and snippets.

liquid-theme-a11y
Implement WCAG 2.2 accessibility patterns in Shopify Liquid themes with semantic HTML and ARIA.

agentation
Add a visual feedback toolbar to Next.js projects for real-time annotation and agent integration.

agentation-self-driving
Autonomously critique web pages by adding design annotations via the Agentation toolbar in a visible browser.

typeui-fundamentals
Universal UI/UX design principles covering visual hierarchy, interaction laws, typography foundations, and WCAG accessibility requirements. Use when making design decisions not covered by a specific design system, validating principle compliance, or resolving conflicts between aesthetics and accessibility. Design-system-agnostic and applies to every surface.

better-icons
Search and retrieve SVGs from 200+ icon libraries via Iconify CLI and MCP server.