
Liquid Theme Standards
- 2.3k installs
- 115 repo stars
- Updated July 16, 2026
- benjaminsehl/liquid-skills
liquid-theme-standards is an agent skill that CSS, JavaScript, and HTML coding standards for Shopify Liquid themes. Covers BEM naming inside stylesheet tags, design t.
About
CSS JS HTML Standards for Shopify Liquid Themes 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 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 liquid comment Do inline variables endcomment div class hero style bg color section settings bg_color padding section settings padding px The liquid theme standards agent skill provides documented workflows prerequisites triggers and safety guidance from its SKILL md source Agents load it when user requests match the description and follow step by step instructions without inventing capabilities It integrates with standard agent tooling for the tasks inputs outputs and failure modes described in the repository documentation
- description: "CSS, JavaScript, and HTML coding standards for Shopify Liquid themes. Covers BEM naming inside stylesheet
- 1. **Progressive enhancement** — semantic HTML first, CSS second, JS third
- 2. **No external dependencies** — native browser APIs only for JavaScript
- Follow liquid-theme-standards SKILL.md steps and documented constraints.
- Follow liquid-theme-standards SKILL.md steps and documented constraints.
Liquid Theme Standards by the numbers
- 2,279 all-time installs (skills.sh)
- +64 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #475 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
liquid-theme-standards capabilities & compatibility
- Capabilities
- description: "css, javascript, and html coding s · 1. **progressive enhancement** — semantic html f · 2. **no external dependencies** — native browser · follow liquid theme standards skill.md steps and
- Use cases
- orchestration
What liquid-theme-standards says it does
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
1. **Progressive enhancement** — semantic HTML first, CSS second, JS third
2. **No external dependencies** — native browser APIs only for JavaScript
npx skills add https://github.com/benjaminsehl/liquid-skills --skill liquid-theme-standardsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.3k |
|---|---|
| repo stars | ★ 115 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 16, 2026 |
| Repository | benjaminsehl/liquid-skills ↗ |
When should an agent use liquid-theme-standards and what problem does it solve?
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 prog
Who is it for?
Developers invoking liquid-theme-standards as documented in the skill source.
Skip if: Skip when requirements fall outside liquid-theme-standards documented scope.
When should I use this skill?
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 prog
What you get
Outputs aligned with the liquid-theme-standards SKILL.md workflow and stated deliverables.
- section Liquid templates
- scoped CSS stylesheets
- settings-driven design tokens
Files
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--largeRules:
- 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
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)
/* 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.jsonPrefer `jq` over `edit` for any .json file modification — it validates structure, handles escaping, and avoids whitespace/formatting issues.
References
- CSS patterns and examples
- JavaScript patterns and examples
CSS Patterns for Shopify Liquid Themes
Complete Component Example
<section
class="featured-collection"
style="
--section-padding: {{ section.settings.padding | default: 60 }}px;
--columns: {{ section.settings.columns | default: 4 }};
"
>
{% if section.settings.heading != blank %}
<h2 class="featured-collection__heading">{{ section.settings.heading }}</h2>
{% endif %}
<div class="featured-collection__grid">
{% for product in collection.products limit: section.settings.limit %}
{% render 'product-card', product: product %}
{% endfor %}
</div>
</section>
{% stylesheet %}
.featured-collection {
padding-block: var(--section-padding);
container-type: inline-size;
}
.featured-collection__heading {
font-size: var(--font-size-2xl);
margin-block-end: var(--space-lg);
text-align: center;
}
.featured-collection__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--space-md);
}
@container (min-width: 768px) {
.featured-collection__grid {
grid-template-columns: repeat(var(--columns), 1fr);
}
}
@media (prefers-reduced-motion: reduce) {
.featured-collection * {
transition: none !important;
}
}
{% endstylesheet %}Layout Patterns
CSS Grid for Page Layouts
.section-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--space-lg);
}Flexbox for Component Layouts
.product-card {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}Page-Width Container
.page-width {
width: min(100%, var(--page-width));
margin-inline: auto;
padding-inline: var(--space-md);
}Full-Bleed with Content Constraint
.full-bleed {
display: grid;
grid-template-columns: var(--space-md) 1fr var(--space-md);
}
.full-bleed > * {
grid-column: 2;
}
.full-bleed > .full-width {
grid-column: 1 / -1;
}Responsive Images
.image-container {
position: relative;
aspect-ratio: 4 / 3;
overflow: hidden;
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}Using :is() for Parent-Child Relationships
/* Multiple parents, same child */
:is(.hero, .banner) .heading {
font-size: var(--font-size-2xl);
}
/* Same parent, multiple children */
.card:is(.card--featured, .card--promoted) {
border: 2px solid var(--color-accent);
}Don't use :is() for simple comma-separated selectors — use regular comma separation instead.
Variable Override Pattern
Use CSS variables to reduce redundancy across modifiers:
.button {
background: rgb(var(--button-color) / var(--button-opacity, 1));
color: rgb(var(--button-text));
}
.button--secondary {
--button-color: var(--color-secondary);
--button-text: var(--color-foreground);
}
.button--outline {
--button-color: transparent;
--button-text: var(--color-accent);
--button-opacity: 0;
}Focus Styles
:focus-visible {
outline: 2px solid rgb(var(--color-focus));
outline-offset: 2px;
}
/* High contrast mode */
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid LinkText;
}
}Print Styles
@media print {
.no-print,
.cart-drawer,
.navigation__mobile {
display: none !important;
}
a[href^='http']::after {
content: ' (' attr(href) ')';
}
.product-card {
break-inside: avoid;
}
}Animation Patterns
/* Safe defaults — only animate transform and opacity */
.product-card {
transition: transform 0.2s ease;
}
.product-card:hover {
transform: translateY(-2px);
}
/* will-change only during animation */
.product-card:hover {
will-change: transform;
}
.product-card:not(:hover) {
will-change: auto;
}CSS Documentation
/* =============================================================================
Product Card
============================================================================= */
/**
* Card component for displaying product information.
*
* @example
* <div class="product-card product-card--featured">
* <div class="product-card__image">...</div>
* <div class="product-card__info">...</div>
* </div>
*/
.product-card { }JavaScript Patterns for Shopify Liquid Themes
Web Component Lifecycle
class MyComponent extends HTMLElement {
#abortController = null;
connectedCallback() {
this.#abortController = new AbortController();
this.#setup();
}
disconnectedCallback() {
this.#abortController?.abort();
// Clean up all resources
}
#setup() {
// Initialize refs, bind events
}
}
customElements.define('my-component', MyComponent);Event-Driven Architecture
Custom Events with Typed Details
/**
* @typedef {Object} CartUpdateDetail
* @property {number} itemCount - Total items in cart
* @property {number} totalPrice - Cart total in cents
*/
// Dispatching
/** @type {CustomEvent<CartUpdateDetail>} */
const event = new CustomEvent('cart:updated', {
detail: { itemCount: 3, totalPrice: 4500 },
bubbles: true
});
this.dispatchEvent(event);
// Listening
document.addEventListener('cart:updated', (event) => {
const { itemCount, totalPrice } = event.detail;
this.#updateDisplay(itemCount, totalPrice);
});Event Naming Convention
Use namespace:action format:
cart:item-added,cart:updated,cart:emptiedvariant:selected,variant:unavailablefilter:applied,filter:clearedsearch:submitted,search:results-loaded
Data Loading Pattern
class ProductLoader extends HTMLElement {
#controller = null;
async load(url) {
this.#controller?.abort();
this.#controller = new AbortController();
this.setAttribute('aria-busy', 'true');
try {
const response = await fetch(url, {
signal: this.#controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const newContent = doc.querySelector('.product-grid');
if (newContent) {
this.querySelector('.product-grid')?.replaceWith(newContent);
}
return newContent;
} catch (error) {
if (error.name === 'AbortError') return null;
console.error('Load error:', error);
throw error;
} finally {
this.setAttribute('aria-busy', 'false');
}
}
disconnectedCallback() {
this.#controller?.abort();
}
}URL Manipulation
// Reading URL parameters
const url = new URL(window.location.href);
const filter = url.searchParams.get('filter');
// Updating URL parameters
const updateURL = (params) => {
const url = new URL(window.location.href);
for (const [key, value] of Object.entries(params)) {
if (value != null) {
url.searchParams.set(key, value);
} else {
url.searchParams.delete(key);
}
}
history.pushState(null, '', url.toString());
};
// Never do this:
// let url = window.location.pathname + '?filter=' + value;Optimistic UI
async addToCart(variantId) {
// 1. Update UI immediately
this.#setButtonState('adding');
this.#incrementCartCount();
try {
// 2. Make request
const formData = new FormData();
formData.append('id', variantId);
formData.append('quantity', '1');
const response = await fetch('/cart/add.js', {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed');
// 3. Confirm success
this.#setButtonState('added');
} catch (error) {
// 4. Revert on failure
this.#setButtonState('error');
this.#decrementCartCount();
console.error('Add to cart failed:', error);
}
}Debounce Pattern
/**
* @param {Function} func - Function to debounce
* @param {number} wait - Delay in milliseconds
* @returns {Function} Debounced function
*/
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
// Usage: search input (300ms), resize handler (150ms)
const handleSearch = debounce((query) => {
// Perform search
}, 300);Intersection Observer (Lazy Loading)
class LazyLoader extends HTMLElement {
#observer;
connectedCallback() {
this.#observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
this.#loadContent(entry.target);
this.#observer.unobserve(entry.target);
}
}
},
{ rootMargin: '200px' }
);
for (const el of this.querySelectorAll('[data-lazy]')) {
this.#observer.observe(el);
}
}
disconnectedCallback() {
this.#observer?.disconnect();
}
#loadContent(element) {
const src = element.dataset.lazy;
if (element instanceof HTMLImageElement) {
element.src = src;
}
}
}JSDoc Type Annotations
/**
* @typedef {Object} ProductData
* @property {string} id - Product ID
* @property {string} title - Product title
* @property {number} price - Price in cents
* @property {boolean} available - Whether in stock
* @property {string[]} tags - Product tags
*/
/**
* Formats a price from cents to display string.
* @param {number} cents - Price in cents
* @param {string} [currency='USD'] - Currency code
* @returns {string} Formatted price
*/
const formatPrice = (cents, currency = 'USD') => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(cents / 100);
};Error Handling
// Always wrap fetch in try/catch
const fetchJSON = async (url, options = {}) => {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') return null;
console.error(`Fetch error for ${url}:`, error);
return null;
}
};
// Validate DOM elements before use
const getElement = (selector, context = document) => {
const element = context.querySelector(selector);
if (!element) {
console.warn(`Element not found: ${selector}`);
}
return element;
};File Organization
Group related classes in feature files:
// cart.js — all cart-related components
class CartDrawer extends HTMLElement { }
class CartItem extends HTMLElement { }
class CartCount extends HTMLElement { }
customElements.define('cart-drawer', CartDrawer);
customElements.define('cart-item', CartItem);
customElements.define('cart-count', CartCount);Related skills
Forks & variants (1)
Liquid Theme Standards has 1 known copy in the catalog totaling 119 installs. They canonicalize to this original listing.
- shopify - 119 installs
How it compares
Pick liquid-theme-standards over generic CSS skills when authoring Shopify Liquid section files that need settings-driven tokens and {% stylesheet %} co-location.
FAQ
What is liquid-theme-standards?
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, def
When should I use liquid-theme-standards?
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, def
Is liquid-theme-standards safe to install?
Review the Security Audits panel on this page before production use.