theme-shopify-javascript-standards
Shopify JavaScript Standards
JavaScript file structure, custom elements, and coding standards for Shopify theme development.
When to Use
- Writing JavaScript for Shopify theme sections
- Creating interactive components
- Setting up JavaScript file structure
- Using custom HTML elements
File Structure
Separate JavaScript Files
- JavaScript must live in a separate file in the
assets/directory - Include it in the section using the
asset_urlfilter
Including JavaScript in Sections
<script src="{{ 'section-logic.js' | asset_url }}" defer="defer"></script>
Always use defer to ensure scripts load after HTML parsing.
File Naming
Match JavaScript file names to section names:
sections/
└── product-quick-view.liquid
assets/
└── product-quick-view.js
JavaScript Rules
Variable Declarations
- Use only
constandlet - Never use
var - Avoid global scope pollution
Example
// Good
const productId = document.querySelector('[data-product-id]').dataset.productId;
let isOpen = false;
// Bad
var productId = ...; // Never use var
window.myVariable = ...; // Avoid global pollution
Scope Management
Keep variables scoped to their usage:
// Good - scoped within function
function initProductCard() {
const card = document.querySelector('.product-card');
const button = card.querySelector('.product-card__button');
// ...
}
// Bad - global variables
const card = document.querySelector('.product-card'); // Global scope
Custom Elements
Use Custom HTML Elements
Use custom HTML elements to encapsulate JavaScript logic and create reusable components.
Custom Element Structure
class ProductQuickView extends HTMLElement {
constructor() {
super();
this.init();
}
init() {
this.button = this.querySelector('[data-trigger]');
this.modal = this.querySelector('[data-modal]');
this.closeButton = this.querySelector('[data-close]');
this.button?.addEventListener('click', () => this.open());
this.closeButton?.addEventListener('click', () => this.close());
}
open() {
this.modal?.classList.add('is-open');
}
close() {
this.modal?.classList.remove('is-open');
}
}
customElements.define('product-quick-view', ProductQuickView);
Using Custom Elements in Liquid
<product-quick-view data-product-id="{{ product.id }}">
<button data-trigger>Quick View</button>
<div data-modal class="modal">
<button data-close>Close</button>
<!-- Modal content -->
</div>
</product-quick-view>
Custom Element Benefits
- Encapsulation - logic is self-contained
- Reusability - use anywhere in the theme
- Lifecycle hooks -
connectedCallback,disconnectedCallback - Data attributes - pass data via
data-*attributes
Lifecycle Hooks
class MyComponent extends HTMLElement {
connectedCallback() {
// Element added to DOM
this.init();
}
disconnectedCallback() {
// Element removed from DOM
this.cleanup();
}
init() {
// Setup logic
}
cleanup() {
// Cleanup logic (remove event listeners, etc.)
}
}
Data Attributes
Passing Data to JavaScript
- Use custom HTML tags when appropriate
- Pass dynamic data via
data-*attributes - Access data via
datasetproperty
Example
<product-card
data-product-id="{{ product.id }}"
data-product-handle="{{ product.handle }}"
data-variant-id="{{ product.selected_or_first_available_variant.id }}">
<!-- Content -->
</product-card>
class ProductCard extends HTMLElement {
constructor() {
super();
this.productId = this.dataset.productId;
this.productHandle = this.dataset.productHandle;
this.variantId = this.dataset.variantId;
}
}
Observers
Use Observers Only When Explicitly Requested
Use observers (IntersectionObserver, MutationObserver, etc.) only if explicitly requested by the user.
IntersectionObserver Example
// Only use if explicitly needed
class LazyImage extends HTMLElement {
constructor() {
super();
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage();
this.observer.unobserve(this);
}
});
});
}
connectedCallback() {
this.observer.observe(this);
}
loadImage() {
const img = this.querySelector('img');
img.src = img.dataset.src;
}
}
Best Practices
Event Handling
class ProductForm extends HTMLElement {
constructor() {
super();
this.form = this.querySelector('form');
this.form?.addEventListener('submit', this.handleSubmit.bind(this));
}
handleSubmit(event) {
event.preventDefault();
// Handle form submission
}
disconnectedCallback() {
// Clean up event listeners
this.form?.removeEventListener('submit', this.handleSubmit);
}
}
Error Handling
class ProductCard extends HTMLElement {
init() {
try {
const button = this.querySelector('[data-add-to-cart]');
if (!button) {
console.warn('Add to cart button not found');
return;
}
button.addEventListener('click', this.handleAddToCart.bind(this));
} catch (error) {
console.error('Error initializing product card:', error);
}
}
}
Shopify Theme Documentation
Reference these official Shopify resources:
Complete Example
Section File
{{ 'product-card.css' | asset_url | stylesheet_tag }}
<product-card
data-product-id="{{ product.id }}"
data-product-handle="{{ product.handle }}">
<div class="product-card">
{{ image | image_tag: widths: '360, 720, 1080', loading: 'lazy' }}
<h3>{{ product.title }}</h3>
<button data-add-to-cart>Add to Cart</button>
</div>
</product-card>
<script src="{{ 'product-card.js' | asset_url }}" defer="defer"></script>
JavaScript File
class ProductCard extends HTMLElement {
constructor() {
super();
this.productId = this.dataset.productId;
this.init();
}
init() {
const button = this.querySelector('[data-add-to-cart]');
button?.addEventListener('click', () => this.addToCart());
}
async addToCart() {
try {
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: this.productId,
quantity: 1
})
});
// Handle response
} catch (error) {
console.error('Error adding to cart:', error);
}
}
}
customElements.define('product-card', ProductCard);
Instructions
- Separate JS files - one file per section in
assets/directory - Use
deferwhen including scripts - Use
constandlet- nevervar - Use custom elements to encapsulate logic
- Pass data via
data-*attributes - Avoid global scope pollution
- Use observers only when explicitly requested
- Clean up event listeners in
disconnectedCallback
More from niccos-shopify-workspace/shopify-cursor-skills
app-shopify-polaris-web-components
Use Shopify Polaris Web Components (s-* custom elements) for App Home UI. Use when building App Home surfaces (not embedded apps), designing UI with s-page, s-section, s-stack, s-box, s-button, and other s-* components. Do not use @shopify/polaris React - App Home requires Web Components.
23app-shopify-app-api-patterns
Frontend-backend communication patterns in Shopify Remix apps. Use when adding pages that need backend data, creating data mutations, using loaders and actions, handling authenticated requests, or managing session and authentication in routes.
16