Assets: CSS, JS, and Front-End Architecture Intermediate 10 min read

Working with Shopify's Built-in JS Utilities

Learn to leverage Shopify's built-in JavaScript utilities and APIs for cart operations, section rendering, currency formatting, and more.

Shopify provides several JavaScript utilities and APIs that power common theme functionality. Understanding these tools helps you build features that integrate seamlessly with Shopify’s platform.

The Shopify Global Object

Shopify injects a global Shopify object with useful properties:

// Store information
Shopify.shop; // 'your-store.myshopify.com'
Shopify.locale; // 'en'
Shopify.currency; // { active: 'USD', rate: '1.0' }
// Route helpers
Shopify.routes.root; // '/' or '/en/' for multi-language
// Money formatting
Shopify.formatMoney(cents, format);

Route Helpers

Access common routes dynamically:

<script>
window.routes = {
root: '{{ routes.root_url }}',
cart: '{{ routes.cart_url }}',
cartAdd: '{{ routes.cart_add_url }}',
cartChange: '{{ routes.cart_change_url }}',
cartClear: '{{ routes.cart_clear_url }}',
search: '{{ routes.search_url }}',
predictiveSearch: '{{ routes.predictive_search_url }}'
};
</script>

Use in JavaScript:

const response = await fetch(window.routes.cartAdd, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: variantId, quantity: 1 }),
});

Cart AJAX API

Add to Cart

async function addToCart(variantId, quantity = 1, properties = {}) {
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: variantId,
quantity: quantity,
properties: properties,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.description || 'Could not add to cart');
}
return response.json();
}
// Usage
try {
const item = await addToCart(12345678, 2, {
_gift_wrap: 'Yes',
Engraving: 'Happy Birthday!',
});
console.log('Added:', item);
} catch (error) {
console.error(error.message);
}

Get Cart

async function getCart() {
const response = await fetch('/cart.js');
return response.json();
}
// Returns cart object with items, totals, etc.
const cart = await getCart();
console.log('Items:', cart.item_count);
console.log('Total:', cart.total_price);

Update Cart

// Update specific line item by key
async function updateCartItem(key, quantity) {
const response = await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: key, quantity }),
});
return response.json();
}
// Update multiple items at once
async function updateCart(updates) {
const response = await fetch('/cart/update.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ updates }),
});
return response.json();
}
// Usage: updates is an object of variant_id: quantity
await updateCart({
12345678: 3, // Set variant 12345678 to quantity 3
87654321: 0, // Remove variant 87654321
});

Clear Cart

async function clearCart() {
const response = await fetch('/cart/clear.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
return response.json();
}

Section Rendering API

Fetch rendered HTML for sections without a full page reload:

async function renderSection(sectionId, params = '') {
const url = `${window.location.pathname}?sections=${sectionId}${params}`;
const response = await fetch(url);
const data = await response.json();
return data[sectionId];
}
// Usage: Update cart drawer after adding item
const cartHtml = await renderSection('cart-drawer');
document.querySelector('#cart-drawer').innerHTML = cartHtml;

Multiple Sections

async function renderSections(sectionIds) {
const url = `${window.location.pathname}?sections=${sectionIds.join(',')}`;
const response = await fetch(url);
return response.json();
}
// Usage
const sections = await renderSections(['cart-drawer', 'cart-icon-bubble']);
document.querySelector('#cart-drawer').innerHTML = sections['cart-drawer'];
document.querySelector('#cart-icon').innerHTML = sections['cart-icon-bubble'];

With URL Parameters

For product pages, pass variant selection:

async function renderProductSections(sectionIds, variantId) {
const url = `${window.location.pathname}?variant=${variantId}&sections=${sectionIds.join(',')}`;
const response = await fetch(url);
return response.json();
}

Predictive Search API

async function predictiveSearch(query, options = {}) {
const {
resources = 'product,collection,article,page',
limit = 4,
unavailableProducts = 'hide',
} = options;
const params = new URLSearchParams({
q: query,
'resources[type]': resources,
'resources[limit]': limit,
'resources[options][unavailable_products]': unavailableProducts,
});
const response = await fetch(`/search/suggest.json?${params}`);
return response.json();
}
// Usage
const results = await predictiveSearch('shirt');
console.log(results.resources.results.products);
console.log(results.resources.results.collections);

Rendering Predictive Search Results

async function getPredictiveSearchHtml(query) {
const url = `/search/suggest?q=${encodeURIComponent(query)}&section_id=predictive-search`;
const response = await fetch(url);
if (!response.ok) return '';
const text = await response.text();
// Extract the section content
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
return doc.querySelector('.predictive-search')?.innerHTML || '';
}

Money Formatting

Shopify provides a formatMoney function:

// Format cents to money string
Shopify.formatMoney(1999, '${{amount}}'); // '$19.99'
Shopify.formatMoney(1999, '${{amount_no_decimals}}'); // '$20'

Setting Up Money Format

<script>
window.Shopify = window.Shopify || {};
window.Shopify.money_format = {{ shop.money_format | json }};
</script>

Custom Money Formatter

For more control:

function formatMoney(cents, format = window.Shopify?.money_format || '${{amount}}') {
if (typeof cents === 'string') cents = cents.replace('.', '');
const placeholders = {
amount: (cents / 100).toFixed(2),
amount_no_decimals: Math.round(cents / 100),
amount_with_comma_separator: (cents / 100).toFixed(2).replace('.', ','),
amount_no_decimals_with_comma_separator: Math.round(cents / 100)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ','),
};
let formattedValue = format;
for (const [key, value] of Object.entries(placeholders)) {
formattedValue = formattedValue.replace(`{{${key}}}`, value);
}
return formattedValue;
}

Product Form Handling

Standard Form Submission

<form action="/cart/add" method="post" id="product-form">
<input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}" />
<input type="number" name="quantity" value="1" min="1" />
<button type="submit">Add to Cart</button>
</form>

AJAX Form Submission

class ProductForm extends HTMLElement {
constructor() {
super();
this.form = this.querySelector('form');
this.submitButton = this.querySelector('[type="submit"]');
}
connectedCallback() {
this.form.addEventListener('submit', this.onSubmit.bind(this));
}
async onSubmit(event) {
event.preventDefault();
this.submitButton.disabled = true;
this.submitButton.textContent = 'Adding...';
try {
const formData = new FormData(this.form);
const response = await fetch('/cart/add.js', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Could not add to cart');
}
// Dispatch event for cart drawer, etc.
document.dispatchEvent(new CustomEvent('cart:updated'));
this.submitButton.textContent = 'Added!';
setTimeout(() => {
this.submitButton.textContent = 'Add to Cart';
this.submitButton.disabled = false;
}, 2000);
} catch (error) {
console.error(error);
this.submitButton.textContent = 'Error';
this.submitButton.disabled = false;
}
}
}
customElements.define('product-form', ProductForm);

Variant Selection

Getting Variant by Options

<script type="application/json" id="product-variants">
{{ product.variants | json }}
</script>
const variants = JSON.parse(document.getElementById('product-variants').textContent);
function getVariantByOptions(options) {
return variants.find((variant) => {
return options.every((option, index) => {
return variant.options[index] === option;
});
});
}
// Usage
const selectedOptions = ['Large', 'Blue'];
const variant = getVariantByOptions(selectedOptions);
if (variant) {
console.log('Found variant:', variant.id, variant.price);
}

Updating URL with Variant

function updateUrl(variantId) {
const url = new URL(window.location.href);
url.searchParams.set('variant', variantId);
history.replaceState({}, '', url);
}

Complete Cart Drawer Example

class CartDrawer extends HTMLElement {
constructor() {
super();
this.drawer = this.querySelector('.cart-drawer__content');
}
connectedCallback() {
// Listen for cart updates
document.addEventListener('cart:updated', () => this.refresh());
// Handle quantity changes
this.addEventListener('change', this.onQuantityChange.bind(this));
// Handle remove buttons
this.addEventListener('click', this.onClick.bind(this));
}
async refresh() {
try {
const sections = await this.renderSections();
this.drawer.innerHTML = sections['cart-drawer'];
this.updateCartCount(sections['cart-icon-bubble']);
} catch (error) {
console.error('Failed to refresh cart:', error);
}
}
async renderSections() {
const response = await fetch('/?sections=cart-drawer,cart-icon-bubble');
return response.json();
}
updateCartCount(html) {
const bubble = document.querySelector('.cart-icon-bubble');
if (bubble) bubble.outerHTML = html;
}
async onQuantityChange(event) {
const input = event.target;
if (!input.matches('[data-quantity-input]')) return;
const key = input.dataset.key;
const quantity = parseInt(input.value);
await this.updateItem(key, quantity);
}
async onClick(event) {
const removeButton = event.target.closest('[data-remove]');
if (!removeButton) return;
event.preventDefault();
const key = removeButton.dataset.key;
await this.updateItem(key, 0);
}
async updateItem(key, quantity) {
this.classList.add('is-loading');
try {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: key, quantity }),
});
await this.refresh();
} catch (error) {
console.error('Update failed:', error);
} finally {
this.classList.remove('is-loading');
}
}
open() {
this.setAttribute('open', '');
document.body.classList.add('cart-drawer-open');
this.refresh();
}
close() {
this.removeAttribute('open');
document.body.classList.remove('cart-drawer-open');
}
}
customElements.define('cart-drawer', CartDrawer);

Error Handling Best Practices

async function safeCartOperation(operation) {
try {
const result = await operation();
return { success: true, data: result };
} catch (error) {
console.error('Cart operation failed:', error);
// Parse Shopify error format
let message = 'Something went wrong. Please try again.';
if (error.message) {
message = error.message;
}
return { success: false, error: message };
}
}
// Usage
const result = await safeCartOperation(() => addToCart(variantId, quantity));
if (result.success) {
showNotification('Added to cart!', 'success');
} else {
showNotification(result.error, 'error');
}

Practice Exercise

Build a “Quick Add” button that:

  1. Adds a product to cart via AJAX
  2. Shows loading state
  3. Updates the cart icon count
  4. Handles errors gracefully
class QuickAddButton extends HTMLElement {
connectedCallback() {
this.button = this.querySelector('button');
this.variantId = this.dataset.variantId;
this.button.addEventListener('click', () => this.addToCart());
}
async addToCart() {
this.button.disabled = true;
this.button.textContent = 'Adding...';
try {
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: this.variantId, quantity: 1 }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.description);
}
// Update cart count
const cart = await fetch('/cart.js').then((r) => r.json());
document.querySelector('.cart-count').textContent = cart.item_count;
this.button.textContent = 'Added!';
setTimeout(() => this.reset(), 2000);
} catch (error) {
this.button.textContent = error.message || 'Error';
setTimeout(() => this.reset(), 3000);
}
}
reset() {
this.button.disabled = false;
this.button.textContent = 'Add to Cart';
}
}
customElements.define('quick-add-button', QuickAddButton);

Key Takeaways

  1. Use Shopify.routes for dynamic route references
  2. Cart AJAX API enables add, update, and clear without page reload
  3. Section Rendering API fetches fresh HTML for dynamic updates
  4. Predictive Search API powers search-as-you-type
  5. formatMoney handles currency formatting
  6. Always handle errors with user-friendly messages
  7. Dispatch custom events for component communication
  8. Use FormData for submitting product forms

What’s Next?

With Shopify’s JavaScript utilities mastered, the next lesson covers Performance Fundamentals: Critical Rendering for optimizing your theme’s load speed.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...