Having a nice looking theme is great, but the real purpose of your Store is to sell your products.

Depending on your business, you may be selling single products or variants of those products, or perhaps a combination of the two. The markup required to add items to the cart will vary depending on how your products have been set up.

Single Product, Single Variant

In a one-to-one product-to-variant setup, your add to cart form will look something like this:

{% if product.active and product.variants.first.state == 'active' %}
  <form action="/cart/items" method="post">
    {% csrf_param %}
    <div>
      <div class="quantity">
        <h6>Quantity</h6>
        <input type="number" name="quantity" value="1" title="Qty"/>
      </div>
      <button type="submit" class="add_to_cart_button single_add_to_cart_button">Add to cart</button>
      <input type="hidden" name="variant_id" value="{{ product.variants.first.id }}" />
    </div>
  </form>
{% endif %}

The first line of markup is an if statement that checks to make sure that the current product and its variant(s) are both active. If they are, the rest of the markup will render.

The form has an action of "/cart/items" and uses the "post" method. Regardless of any styling, these attributes and values must be present in order for the form to work correctly.

<form action="/cart/items" method="post">

Nested within the form is a special tag, the {% csrf_param %} . This is another essential element that ensures no malicious code is executed when the add to cart form is submitted.

{% csrf_param %}

The next essential element is the "quantity" input . It's imperative that this element have a name of "quantity".

<input type="number" name="quantity" value="1" title="Qty"/>

Because we know that this product has only one variant, we can use a hidden input to send the correct variant in the form data. This input can be placed anywhere within the form.

<input type="hidden" name="variant_id" value="{{ product.variants.first.id }}" />

The submit button is the last necessary element. As with any HTML form, a button tag with a type of "submit" will send the form and all of its data to the server.

<button type="submit">Add to cart</button>

Single Product, Multiple Variants

A product with more than one variant requires a more complex form and Javascript.

N.B. This example uses Bootstrap classes, but you're free to use whatever styling you'd like.

<script type="text/javascript">var product = {{ product | json_object }}</script>
<form id="add_to_cart_form" class="add-to-cart-form">
  {% csrf_param %}
  <!-- Render option select lists if the product has more than one option -->
  {% if product.options.size == 0 %}
    <input type="hidden" name="variant_id" value="{{ product.variants.first.id }}" id="variant_id" class="form-control" />
  {% elsif product.options.size > 1 %}
    <input type="hidden" name="variant_id" value="" id="variant_id" class="form-control" />
    <div class="alert alert-warning variant-not-available">This item is currently unavailable. Please select another option.</div>
    {% for option in product.options %}
      <label>{{ option.name }}</label>
      <select name="options[][{{ option.guid }}]" class="form-control variant-selector">
        <option value="">Select {{ option.name }}</option>
        {% for val in option.values %}
          <option value="{{ val | replace: '"', '&quot;' }}">{{ val }}</option>
        {% endfor %}
      </select>
    {% endfor %}
  <!-- If the product has just one option, render variants in a select list -->
  {% elsif product.options.size == 1 %}
    {% assign option_name = product.options.first.name %}
    <div class="input-group">
      <label for="variant_id">Select a {{ option_name }}</label>
      <select name="variant_id" id="variant_id" class="form-control single-option-variant-selector">
        <!-- Only show variants that are active -->
        {% for variant in product.variants %}
          {% if variant.state == 'active' %}
            <option value="{{variant.id}}" data-variant-price="{{ variant.price }}">{{variant.description}}</option>
          {% endif %}
        {% endfor %}
      </select>
    </div>
  {% endif %}
  <div class="add-to-cart">
    <label class="prod-options">Quantity</label>
    <div class="input-group">
      <input class="form-control" type="number" name="quantity" value="1" style="text-align: center;" />
      <input type="hidden" name="product_id" value="{{ product.id }}" />
      {% if product.options.size == 0 and product.variants.first.state == 'active' %}
        <input type="hidden" name="variant_id" value="{{ product.variants.first.id }}" />
      {% endif %}
      <span class="input-group-btn">
        {% if product.options.size > 1 %}
          <a class="btn btn-success add-to-cart-button" disabled="true">Add To Cart</a>
        {% else %}
          <a class="btn btn-success add-to-cart-button">Add To Cart</a>
        {% endif %}
      </span>
    </div>
  </div>
</form>

The first line uses the json_object filter to give us a Javascript product variable with all the relevant data necessary to make changes 

This form uses a select list populated with all product options in order to find the variant that corresponds with the chosen options. The following Javascript watches for changes to the select lists, and inserts the correct values as the user interacts with the form.

$(document).ready(function(){
  Number.prototype.formatMoney = function(c, d, t){
    var n = this,
        c = isNaN(c = Math.abs(c)) ? 2 : c,
        d = d == undefined ? "." : d,
        t = t == undefined ? "," : t,
        s = n < 0 ? "-" : "",
        i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "",
        j = (j = i.length) > 3 ? j % 3 : 0;
       return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
   };

  findSelectedVariant = function(){
    selectedVariant = $.grep(product.variants, function(variant) {
      if(product.options.every(function(opt) { return variant.opts[opt.guid] == opt.selectedValue })) {
        return variant
      }
    })[0];

    if((selectedVariant && selectedVariant.state != 'active')){
      $('.variant-not-available').css('display', 'block');
      $('.add-to-cart-button').attr('disabled', 'true').removeClass('active-button')
    } else if(selectedVariant) {
      $('#variant_id').val(selectedVariant.id)
      $('.variant-not-available').css('display', 'none');
      $('.add-to-cart-button').removeAttr('disabled').addClass('active-button')

      $('.price-label').text("$" + selectedVariant.price.formatMoney());
    }
  };

  $('select.variant-selector').on('change', function(e){
    guid = e.target.name.replace("options[][","").replace("]","");
    opt = $.grep(product.options, function(opt) { return opt.guid == guid })[0]

    if(opt) {
      opt.selectedValue = e.target.value;
    }
    if (opt.selectedValue != "") {
      return findSelectedVariant();
    } else {
      $('.add-to-cart-button').attr('disabled', 'true').removeClass('active-button');
    }
  });

  $('select.single-option-variant-selector').on('change', function(e){
    singleSelectedVariant = $(($('select.single-option-variant-selector option:selected')[0]))
    selectedVariantPrice = parseFloat(singleSelectedVariant.data('variant-price'))
    $('.price-label').text("$" + selectedVariantPrice.formatMoney())
  });

  $('.add-to-cart-button').on('click', function() {
    $.ajax({
      method: 'POST',
      url: '/cart/items.json',
      data: $('#add_to_cart_form').serialize(),
      success: function(res) {
        console.log(res);
      },
      error: function(res) {
        console.log(res);
      }
    })
  })
})

This form is submitted asynchronously, meaning the page is not refreshed and/or the browser is not redirected, using an AJAX request in Javascript. Doing this allows you to keep users on the product page they're currently viewing, and you can notify them that their product has been successfully added to the cart in the success callback of the AJAX request. You might use that callback to display a small alert or message to your users.

Did this answer your question?