Loading...
Loading...
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.
npx skill4agent add benjaminsehl/liquid-skills liquid-theme-standards| Location | Liquid? | Use For |
|---|---|---|
| No | Component-scoped styles (one per file) |
| Yes | Dynamic values needing Liquid (e.g., color settings) |
| No | Shared/global styles |
{% stylesheet %}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 %}.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.block__element.block__el1__el2class="btn btn--primary"class="btn--primary"<!-- 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>0 1 00 4 0!important/* 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 */
}
}: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-sm--space-16:root: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;
"
>positiondisplayflex-directiongrid-template-columnswidthmarginpaddingborderfont-familyfont-sizeline-heightcolorbackgroundopacityborder-radiustransitionanimation/* 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;.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 */
}/* 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); }transformopacitywill-changecontain: contentdvhvh@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;
}
}| Location | Liquid? | Use For |
|---|---|---|
| No | Component-specific scripts (one per file) |
| No | Shared utilities, Web Components |
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>| Rule | Do | Don't |
|---|---|---|
| Loops | | |
| Async | | |
| Variables | | |
| Conditionals | Early returns | Nested |
| URLs | | String concatenation |
| Dependencies | Native browser APIs | External libraries |
| Private methods | | |
| Types | JSDoc | Untyped |
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
}));| Need | Use | Not |
|---|---|---|
| Expandable | | Custom accordion with JS |
| Dialog/modal | | Custom overlay div |
| Tooltip/popup | | Custom positioned div |
| Search form | | |
| Form results | | |
{%- 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"widthheightaltalt=""templates/*.jsonsections/*.jsonconfig/settings_data.jsonjqbash# 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.jsonjqedit.json