Shopify - Create a custom variant selector
Tags: shopify, liquid, javascript, css
Published: 2021-01-15 10:33:54

*IWIK : I Wish I Knew*

## Table of content
1. [Custom variant size selector](#custom-variant-size-selector)
 - [Configuration](#product-configuration)
 - [Liquid template markup](#liquid-template-markup)
 - [Basic styling](#basic-styling)
 - [Proper behavior with Javascript](#proper-behavior-with-javascript)
 - [Insights](#insights)
1. [Shopify custom variant with images](#shopify-custom-variant-with-images)
 - [Liquid implementation and explanations](#liquid-implementation-and-explanations)
 - [The Javascript part](#the-javascript-part)

## Custom variant size selector

### Introduction

For a client's e-commerce project I had the opportunity to work with Shopify. It was my first time using it and while the overall experience was great, I came across several use cases where I thought beforehand that it would be simpler.

Today's subject is a simple looking feature that is so common that it can be overlooked when estimating the amount of work / customization you need in a product page. A **variant selector** and, more precisely, what the next lines describes, a **size chart selector**. Using the [Debut theme](https://themes.shopify.com/themes/debut/styles/default)* where the default style for this element is a dropdown list. 

**The Liquid markup and Javascript used in this tutorial are based on the Debut theme.*

Here's the result we want to obtain :

![output](https://dev-to-uploads.s3.amazonaws.com/i/rcuczswz3pv1e8n2yrn3.png)
 

Now, it is a common display that's on a lot of Shopify based e-commerce websites but when it's your first time, the new terms might get confusing. I am perfectly aware that it is not the trickiest feature but having an article like this could have helped me save a few hours of research.

### Product configuration

To a new or existing product **add a variant** named **"size"** (name is important here, if you want another name be sure to replace "size" in the example code with your custom name) with multiple options :

![variants](https://dev-to-uploads.s3.amazonaws.com/i/f48eu50ikxy0izkkeny9.png)
 

If you visit this product page you should be able to see a size selector automatically generated, depending on the theme it may match your needs but if you are reading this, I guess not exactly.

### Liquid template markup

We're not going to reinvent the wheel and make good use of what's already in place.

Edit your theme code and take a look inside `/sections/product-template.liquid`: 

![before](https://dev-to-uploads.s3.amazonaws.com/i/objtkx3o1j8g66r0umqr.png)
 

 This code does one thing : for every variant that exists for a product, it generates a dropdown selector. We are going to update this **for loop** in a way that : 

- Our size selector is a specific case with a different markup
- The dropdown select, the default result

That way it won't break any existing or future variant selector.

The liquid code, inside the `product.options_with_values` for loop :

```liquid
<div class="selector-wrapper js product-form__item">
	<p>{{ option.name }}</p>
	  {% if option.name == "Size" %}
	    {% assign index = forloop.index %}
	    <div class="size-selector">
	      <div class="size-selector__list">
	        {% for value in option.values %}
	        {% assign sizeWords = value | split: ' ' %}
	        {% capture sizeLetters %}{% for word in sizeWords %}
	        {{ word | slice: 0 }}
	        {% endfor %}{% endcapture %}
	        <div class="size-selector__item">
	          <input 
               class="single-option-selector-{{ section.id }}" 
               id="size-{{ forloop.index }}" type="radio" name="size" 
               value="{{ value | escape }}" 
               data-index="option{{index}}"
               {% if option.selected_value == value %} checked="true"{% endif %} />
	          <label for="size-{{ forloop.index }}">{{ sizeLetters }}</label>
	        </div>
	        {% endfor %}
	      </div>
	    </div>
	{% else %}
		// The original code without the .selector-wrapper div
	{% endif %}
</div>
```

You should obtain something that looks like this :
![after](https://dev-to-uploads.s3.amazonaws.com/i/101d5zcyhrps6qyo7tyx.png)

Some explanations : 

- Wrap everything in a new `.selector-wrapper` class
- The **IF** condition on line 167 matches the name we gave to our product variant (to be replaced with a custom name if different)
- Instead of a `<select>` we're now using **radio inputs**, one for each of our "size"
- Bonus : only the first letter of each word you use as a size will appear on your selector (ex: Small → S, X Large → XL, etc...)

### Basic styling

Here is the CSS used to obtain the final look (location depends how you manage your css, default in `assets/theme.css`)

```css
.size-selector {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 60px;
}

.size-selector input {
  display: none;
}

.size-selector p {
  font-size: 18px;
  line-height: 57px;
  letter-spacing: 0.56px;
}

.size-selector .selector-label:hover {
  text-decoration: underline;
}

.size-selector__list {
  display: flex;
}

.size-selector__item:hover label,
.size-selector__item input[checked] ~ label {
  border-color: #2A2726;
}

.size-selector__item label {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 32px;
  min-width: 32px;
  margin-right: 16px;
  border-radius: 50%;
  border: 1px solid transparent;
  font-size: 14px;
  line-height: 18px;
  text-align: center;
}
```

Make sure to save all the recent changes. Visit your product page again, refresh and start playing with the size selector. 

### Proper behavior with Javascript

It works without extra Javascript code because it uses the same class `single-option-selector-{{ section.id }}`. The theme should handle radio and checkbox automatically.

If you look at the url you can see a change in the variant ID param. That means it's working but as you can see, the "active" size doesn't get updated. To fix that we are going to need some Javascript to :

- Target our `.size-selector` class
- Toggle the **checked** attribute when the current selected option changes

Find `theme.Product` in `assets/theme.js`

In the **selectors** object add : 

```javascript
this.selectors = {
	...,
	productSizes: '.size-selector',
}
	
```

Scroll down until you find a function named `_initVariants`

In the **options** add : 

```javascript
var options = {
  ...
  productSizesSelector: this.selectors.productSizes,
};
```

Find `slate.variants` in `assets/theme.js`

In the constructor function named **Variants** add : 

```javascript
this.productSizesSelector = this.container.querySelector(options.productSizesSelector);
if (this.productSizesSelector)
  this.singleOptionsSize = this.productSizesSelector.querySelectorAll(options.singleOptionSelector);
```

The first line targets the class selector we have defined in the previous steps. After checking for the existence of a size selector element in the page, it selects all the radio inputs options.

Scroll down to find a function named `_onSelectChange`

Under the line `this.currentVariant = variant` add : 

```javascript
if (event.currentTarget.getAttribute('name') == 'size') {
  this.singleOptionsSize.forEach(function(option) {
    option.removeAttribute('checked');
  });
  event.currentTarget.setAttribute('checked', true);
}
```

On every variant change, if that concern our **size** selector : 

1. Remove the checked attribute from all the options
2. Set the selected option as checked

Be sure to save everything once again, refresh and you should obtain the desired behavior. If it's not the case, double check that : 

- The `.size-selector` class (without the `.`) is present in the loop markup
- The IF condition in the last step `== 'size'` is correct (based on the name of your variant)

### Insights

As I explained earlier a variant selector is only one of the concepts that beginners have to apprehend when learning Shopify.
Most of the pain points I experienced are mostly around the use of Javascript for transitions and animations and might be the subject for another article.

Please let me know if you experience any difficulty creating your own version of the size selector. 

## Shopify custom variant with images

@adhendo in the comments asked how I would handle images. 
It's actually fast to implement.

### Liquid implementation and explanations

In the Shopify Product page administration :
1. Click More options > edit options : add another option (I’ll use **fabric** in my example)
2. Add variant : Fill only the **fabric** field
3. Create and name the images you want with the same name that the variant value
(ex:  
- Variant : red -> Image : fabric_red.jpg
- Variant : Dark blue -> Image : fabric_dark-blue.jpg
- …)
4. Upload your images in the /assets folder (Go to "Online store" on the left sidebar, Actions > edit code > find the assets folder > add asset)


If you look again at my liquid code from the classic variant you will notice on the 3rd line : 
```liquid
{% if option.name == "Size" %}{% endif %}
```
Instead of this we would use 
```liquid
{% if option.name == "Fabric" %}{% endif %}
```

I used a slider library named glide (hence the class names and structure) but you are free to use any style/library later.

Here is my implementation. Explanations are just below this code block.

```liquid
{% if option.name == « Fabric » %}
  <div class="fabric-selector">
    <label class="selector-label">Pick a fabric</label>
    <div class="glide">
      <div class="glide__track" data-glide-el="track">
        {% assign index = forloop.index %}
        <div class="glide__slides">
          {% for value in option.values %}
            <div class="glide__slide">
              <img src="{{ value | handleize | append: '.jpg' | prepend: 'fabric_' | strip | asset_url }}" alt="{{ option.name }}" />
              <input 
                class="single-option-selector-{{ section.id }}" 
                id="color-{{ forloop.index }}" type="radio" name="color" 
                value="{{ value | escape }}" 
                data-index="option{{index}}"
                {% if option.selected_value == value %} checked="true"{% endif %} />
              <label for="color-{{ forloop.index }}">{{ value }}</label>
            </div>
          {% endfor %}
        </div>
      </div>
      <div class="glide__arrows" data-glide-el="controls">
        <span class="glide__arrow glide__arrow--left {% if option.values.size < 2 %}hidden{% endif %}" data-glide-dir="<">
          {% include 'nw-chevron-left' %}
        </span>
        <div class="fabric-selector__data">
          <p class="fabric-selector__data-name">{{ option.values[0] }}</p>
        </div>
        <span class="glide__arrow glide__arrow--right {% if option.values.size < 2 %}hidden{% endif %}" data-glide-dir=">">
          {% include 'nw-chevron-right' %}
        </span>
      </div>
    </div>
  </div>
{% elseif option.name == "Size" %}...{% endif %}
```

Line 10 is where the image gets called. What is happening is details :

**handleize** : if your fabric name has spaces or uppercase it will be transformed to a slug like string for a better image name (ex: Dark grey -> fabric_dark-grey)
**append** : Because all my images are .jpg (but any image format would work)
**prepend** : I prefer to use a prefix for naming my images
**asset_url** : Get that image from the asset folder

### The Javascript part

The implementation will vary depending on how you want to display the images (basic list of elements or in a slider).
For the basic to run properly, follow the steps for the size-selector and change the variable names accordingly (*fabric-selector* in my case).


Thank you for reading 🙏