Cart & Forms

Shopify's cart is a persistent object tied to the shopper's session, and the most basic way to add something to it doesn't require any JavaScript at all — just a correctly-shaped HTML form. This lesson covers that form, the cart object, and where JavaScript enters when you want a smoother experience.

The add-to-cart form

Liquid add to cart
<form action="/cart/add" method="post">
  <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>

Posting to /cart/add with a variant id is all Shopify needs — it's a plain HTTP endpoint Shopify itself handles, no app or custom backend required. Everything else about the form (styling, extra fields) is up to the theme.

The cart object

Once something's in the cart, cart is available on every page — most usefully on a dedicated cart page or in a cart drawer/flyout:

Liquid templates/cart.liquid
<p>{{ cart.item_count }} items — {{ cart.total_price | money }}</p>
<ul>
{% for item in cart.items %}
  <li>{{ item.product.title }} × {{ item.quantity }}</li>
{% endfor %}
</ul>

Ajax add-to-cart

Submitting the plain HTML form causes a full page reload to the cart page — fine functionally, but most themes intercept the submit with JavaScript and call the same endpoint with fetch, then update a cart icon or open a drawer without navigating away:

JavaScript ajax add to cart
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  const res = await fetch('/cart/add.js', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ id: variantId, quantity: 1 })
  });
  const item = await res.json();
  updateCartIcon();
});

Note the .js suffix on the endpoint — Shopify's cart endpoints respond with JSON when called that way instead of issuing a redirect, which is exactly what a fetch-based flow needs. /cart/change.js and /cart/update.js work the same way for adjusting quantities or removing a line entirely.

Line item properties

A form can attach custom data to a cart line — a gift message, an engraving text — with inputs named properties[key]:

Liquid a custom property
<input type="text" name="properties[Gift Message]">

Cart attributes and notes

Beyond individual line items, the cart as a whole can carry attributes (arbitrary key/value data, like a delivery preference) and a single free-text note — both set the same way, through form inputs or the /cart/update.js endpoint, and both visible later to staff in the Shopify Admin order view:

Liquid a cart-wide note field
<form action="/cart" method="post">
  <textarea name="note">{{ cart.note }}</textarea>
  <input type="text" name="attributes[gift_wrap]" value="">
  <button type="submit">Update cart</button>
</form>

Where theme control actually ends

Liquid governs everything up through the cart, but the checkout pages beyond it are a mostly separate system that a theme's templates don't reach into. What a store can still customize at checkout is done through small, sandboxed interface components a merchant places at specific approved spots — a note above the payment section, a message on the order confirmation page — built and deployed independently from the theme itself, using tooling made for that one purpose rather than Liquid.

Note: if a project's requirements sound like "we need to add a custom step or field to checkout," that's a signal to look at what checkout-specific customization tooling currently supports, rather than trying to route around it — checkout is deliberately the most locked-down part of the platform, since it directly handles payment.