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 informationShopify.shop; // 'your-store.myshopify.com'Shopify.locale; // 'en'Shopify.currency; // { active: 'USD', rate: '1.0' }
// Route helpersShopify.routes.root; // '/' or '/en/' for multi-language
// Money formattingShopify.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();}
// Usagetry { 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 keyasync 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 onceasync 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: quantityawait 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 itemconst 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();}
// Usageconst 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}§ions=${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();}
// Usageconst 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)}§ion_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 stringShopify.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; }); });}
// Usageconst 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 }; }}
// Usageconst 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:
- Adds a product to cart via AJAX
- Shows loading state
- Updates the cart icon count
- 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
- Use
Shopify.routesfor dynamic route references - Cart AJAX API enables add, update, and clear without page reload
- Section Rendering API fetches fresh HTML for dynamic updates
- Predictive Search API powers search-as-you-type
formatMoneyhandles currency formatting- Always handle errors with user-friendly messages
- Dispatch custom events for component communication
- 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...