Skip to content
Custom Product Options - Free Tutorial
Browse other ways to boost conversion rate & profit

Custom Product Options - Free Tutorial

In this tutorial, we're adding more product options to your store that you can customize yourself.

With Shopify, you’re limited to 3 product variant options only, and adding more requires you to install apps or hire a developer. So you can use this tutorial to add personalization or as a workaround for Shopify's 100 variant limit.

 

Compatible Themes: This code should work on version 14 and 15 of all free Shopify themes (Dawn, Refresh, Craft, Studio, Publisher, Crave, Origin, Taste, Colorblock, Sense, Ride, Spotlight).

Links: 

 

 

Optional: Enable swatches

Add product swatches functionality to use swatches with the custom options picker in this tutorial. If this is not added, the picker can only have styles pill and dropdown, and swatches will not be available.

Add swatches by following this tutorial.

 

Create metaobject

  • Name: Custom Product Options
    • Field 1
      • Name: Title
      • Type: Single line text
      • One value
    • Field 2
      • Name: Option Name
      • Type: Single line text
      • One value
    • Field 3:
      • Name: Option Values
      • Type: Single line text
      • List of values

Create metafields

  • Name: Custom Input Properties To Enable
    • Type: Singe line text
    • List of values
  • Name: Custom Options Pickers To Enable
    • Type: Metaobject
    • Reference: Custom Product Options
    • List of entries

Edit main-product.liquid

Include js and css assets near top of file

{%- assign has_custom_input = false -%}
{%- for block in section.blocks -%}
  {%- if block.type == 'custom_text_input' or block.type == 'custom_checkbox_input' or block.type == 'custom_options_picker' -%}
    {%- assign has_custom_input = true -%}
    {%- break -%}
  {%- endif -%}
{%- endfor -%}
{%- if has_custom_input -%}
  <script src="{{ 'product-options-custom.js' | asset_url }}" defer="defer"></script>
  {{ 'product-options-custom.css' | asset_url | stylesheet_tag }}
  {{ 'component-product-variant-swatch-custom.css' | asset_url | stylesheet_tag }}
  {% if product.has_only_default_variant %}
    {{ 'component-product-variant-picker.css' | asset_url | stylesheet_tag }}
  {% endif %}
{%- endif -%}

Add new custom options blocks

{%- when 'custom_text_input' -%}
  {% render 'product-options-custom', product: product, block: block %}
{%- when 'custom_checkbox_input' -%}
  {% render 'product-options-custom', product: product, block: block %}
{%- when 'custom_options_picker' -%}   
  {% render 'product-options-custom', product: product, block: block %}

Add new custom options settings

{
  "type": "custom_text_input",
  "name": "Custom Text Input",
  "limit": 3,
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "default": "Custom Text Input",
      "label": "Block Label"
    }, 
    {
      "type": "text",
      "id": "property_name",
      "label": "Property Name",
      "default": "Custom_Text"
    },
    {
      "type": "text",
      "id": "label",
      "label": "Input Label",
      "default": "Custom Text"
    },
    {
      "type": "text",
      "id": "placeholder",
      "label": "Placeholder Text",
      "default": "Enter your custom text"
    }
  ]
},
{
  "type": "custom_checkbox_input",
  "name": "Custom Checkbox Input",
  "limit": 3,
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "default": "Custom Checkbox Input",
      "label": "Block Label"
    },
    {
      "type": "text",
      "id": "property_name",
      "label": "Property Name",
      "default": "Custom_Checkbox"
    },
    {
      "type": "text",
      "id": "label",
      "label": "Input Label",
      "default": "Custom Text"
    },
    {
      "type": "checkbox",
      "id": "default_checked",
      "label": "Default Checked",
      "default": false
    },
    {
      "type": "color",
      "id": "toggle_color",
      "label": "Toggle Color",
      "default": "#4caf50"
    }
  ]
},
{
  "type": "custom_options_picker",
  "name": "Custom Options Picker",
  "limit": 3,
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "default": "Custom Options Picker",
      "label": "Block Label"
    },        
    {
      "type": "text",
      "id": "property_name",
      "label": "Property Name",
      "default": "Custom_Picker"
    }, 
    {
      "type": "select",
      "id": "picker_type",
      "options": [
        {
          "value": "dropdown",
          "label": "t:sections.main-product.blocks.variant_picker.settings.picker_type.options__1.label"
        },
        {
          "value": "button",
          "label": "t:sections.main-product.blocks.variant_picker.settings.picker_type.options__2.label"
        }
      ],
      "default": "button",
      "label": "t:sections.main-product.blocks.variant_picker.settings.picker_type.label"
    },
    {
      "type": "header",
      "content": "Custom Swatch"
    },
    {
      "id": "swatch_shape_custom",
      "label": "Swatch (custom)",
      "type": "select",
      "info": "Variant picker style must be Pills to use the custom swatches", 
      "options": [
        {
          "value": "circle",
          "label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__1.label"
        },
        {
          "value": "square",
          "label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__2.label"
        },
        {
          "value": "none",
          "label": "t:sections.main-product.blocks.variant_picker.settings.swatch_shape.options__3.label"
        }
      ],
      "default": "none"
    },
    {
      "type": "text",
      "id": "variant_swatch_metaobject",
      "label": "Default Variant Swatch Map Metaobject",
      "default": "variant-swatch-mapping",
      "info": "Can be overridden with product metafield"
    },
    {
      "type": "range",
      "id": "swatch_size",
      "min": 30,
      "max": 60,
      "step": 1,
      "unit": "px",
      "label": "Swatch Size",
      "default": 40
    }
  ]
},

Create new liquid snippet product-options-custom.liquid

{%- case block.type -%}

  {%- when 'custom_text_input' -%}
    {% assign custom_input_properties = product.metafields.custom.custom_input_properties_to_enable | remove: '[' | remove: ']' | remove: '"' | split: ',' %}
    {% assign current_property = block.settings.property_name | strip %}
    {% for property in custom_input_properties %}
      {% assign trimmed_property = property | strip %}
      {% if trimmed_property == current_property %}
        <div class="custom-text-input" {{ block.shopify_attributes }}>
          <label for="custom-text-{{ block.id }}" class="form__label">{{ block.settings.label }}</label>
          <input 
            type="text" 
            id="custom-text-{{ block.id }}" 
            class="custom-text-field" 
            data-property-name="{{ block.settings.property_name }}"
            data-hidden-input-id="hidden-{{ block.id }}-{{ block.settings.property_name | handle }}"
            placeholder="{{ block.settings.placeholder }}"
          >
        </div>
        {% break %}
      {% endif %}
    {% endfor %}
           
  {%- when 'custom_checkbox_input' -%}
    {% assign custom_input_properties = product.metafields.custom.custom_input_properties_to_enable | remove: '[' | remove: ']' | remove: '"' | split: ',' %}
    {% assign current_property = block.settings.property_name | strip %}
    {% for property in custom_input_properties %}
      {% assign trimmed_property = property | strip %}
      {% if trimmed_property == current_property %}
        <div class="custom-input-container">
          <label class="custom-checkbox-label form__label">
            <input type="checkbox"
                   id="custom-checkbox-{{ block.id }}"
                   class="custom-checkbox-input"
                   data-block-id="{{ block.id }}"
                   data-hidden-input-id="hidden-{{ block.id }}-{{ block.settings.property_name | handle }}"
                   {% if block.settings.default_checked %}checked="checked"{% endif %}>
            <span class="custom-checkbox-slider" style="--toggle-color: {{ block.settings.toggle_color }};"></span>
            {{ block.settings.label }}
          </label>
          <style>
            #custom-checkbox-{{ block.id }}:checked + .custom-checkbox-slider {
              background-color: {{ block.settings.toggle_color }} !important;
            }
          </style>
        </div>
        {% break %}
      {% endif %}
    {% endfor %}

{%- when 'custom_options_picker' -%}   
  {% assign custom_input_properties = product.metafields.custom.custom_input_properties_to_enable | remove: '[' | remove: ']' | remove: '"' | split: ',' %}
  {% assign current_property = block.settings.property_name | strip %}
  {% for property in custom_input_properties %}
    {% assign trimmed_property = property | strip %}
    {% if trimmed_property == current_property %}
      {% assign custom_options_pickers = product.metafields.custom.custom_options_pickers_to_enable.value %}
      {% assign matching_metaobject = nil %}
      {% for metaobject in custom_options_pickers %}
        {% if metaobject.option_name == current_property %}
          {% assign matching_metaobject = metaobject %}
          {% break %}
        {% endif %}
      {% endfor %}
      {% if matching_metaobject %}  
        {% assign option_name = matching_metaobject.option_name %}
        {% assign option_values = matching_metaobject.option_values.value %}
    	  {% assign target_entry = nil %}
    	  {% for entry in shop.metaobjects.variant_swatch_map.values %}
    	    {% if entry.title == block.settings.variant_swatch_metaobject %}
    	      {% assign target_entry = entry %}
    	      {% break %}
    	    {% endif %}
    	  {% endfor %}
    	  {% if target_entry %}
    	    {% assign variant_images_data = target_entry.variant_images_json %}
    	  {% else %}
    	    {% assign variant_images_data = nil %}
    	  {% endif %}
        {%- if option_values.size > 0 -%}
          {%- if block.settings.picker_type == 'button' -%}
            {% if block.settings.swatch_shape_custom == "none" %}
              <fieldset class="js product-form__input product-form__input--pill" data-block-id="{{ block.id }}" data-option-name="{{ option_name }}">
                <legend class="form__label">{{ option_name }}</legend>
                {% for value in option_values %}
                  <input
                    type="radio"
                    id="option-{{ value | handleize }}-{{ block.id }}"
                    name="custom_option_{{ block.id }}"
                    value="{{ value }}"
                    data-option-value="{{ value }}"
                    data-block-id="{{ block.id }}"
                    data-option-name="{{ option_name | handle }}"
                    {% if forloop.first %}checked="checked"{% endif %}
                  >
                  <label for="option-{{ value | handleize }}-{{ block.id }}">{{ value }}</label>
                {% endfor %}
              </fieldset>
            {% else %}
              {% assign base_store_files_url = '//' | append: shop.permanent_domain | append: '/cdn/shop/files/' %}
              {% assign variant_options_images = variant_images_data.value | where: "variant_name", option_name %}
              <fieldset class="js product-form__input product-form__input--pill" data-block-id="{{ block.id }}" data-option-name="{{ option_name }}">
                <legend class="form__label">
                  {{ option_name }}:
                  <span id="selected{{ option_name }}">{{ option_values.first }}</span>
                </legend>
                {% for value in option_values %}
                  {% assign variant_image_url = nil %}
                  {% assign swatch_found = false %}
                  {% for item in variant_options_images %}
                    {% if item.variant_value == value %}
                      {% if item.variant_swatch != blank %}
                        {% assign variant_image_url = base_store_files_url | append: item.variant_swatch %}
                        {% assign swatch_found = true %}
                      {% elsif item.variant_hex %}
                        {% assign hex_color = item.variant_hex | replace: '#', '%23' %}
                        {% assign svg = '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="100%" height="100%" fill="' | append: hex_color | append: '" /></svg>' %}
                        {% assign encoded_svg = svg | replace: '"', '%22' | replace: '<', '%3C' | replace: '>', '%3E' %}
                        {% assign variant_image_url = 'data:image/svg+xml;charset=utf-8,' | append: encoded_svg %}
                        {% assign swatch_found = true %}
                      {% endif %}
                      {% if swatch_found %}
                        {% break %}
                      {% endif %}
                    {% endif %}
                  {% endfor %}
                  {% if swatch_found %}
                    <div class="product-form__swatch"> 
                      <input
                        type="radio"
                        id="option-{{ value | handleize }}-{{ block.id }}"
                        name="custom_option_{{ block.id }}"
                        value="{{ value }}"
                        data-option-value="{{ value }}"
                        data-block-id="{{ block.id }}"
                        data-option-name="{{ option_name | handle }}"
                        {% if forloop.first %}checked="checked"{% endif %}
                        onclick="document.getElementById('selected{{ option_name }}').innerText = '{{ value }}';"
                      >
                      <label for="option-{{ value | handleize }}-{{ block.id }}" style="background-image: url('{{ variant_image_url }}'); width: {{ block.settings.swatch_size }}px; height: {{ block.settings.swatch_size }}px; border-radius: {% if block.settings.swatch_shape_custom == 'circle' %}50%{% else %}0%{% endif %} !important;">
                        <span class="visually-hidden">{{ value }}</span>
                      </label>
                    </div>
                  {% else %}
                    <input
                      type="radio"
                      id="option-{{ value | handleize }}-{{ block.id }}"
                      name="custom_option_{{ block.id }}"
                      value="{{ value }}"
                      data-option-value="{{ value }}"
                      data-block-id="{{ block.id }}"
                      data-option-name="{{ option_name | handle }}"
                      {% if forloop.first %}checked="checked"{% endif %}
                    >
                    <label for="option-{{ value | handleize }}-{{ block.id }}">{{ value }}</label>
                  {% endif %}
                {% endfor %}
              </fieldset>
            {% endif %}
          {%- elsif block.settings.picker_type == 'dropdown' -%}
            <div class="product-form__input product-form__input--dropdown">
              <label for="OptionSelect-{{ block.id }}" class="form__label">{{ option_name }}</label>
              <div class="select">
                <select
                  id="OptionSelect-{{ block.id }}"
                  name="custom_option_{{ block.id }}"
                  class="select__select"
                  data-block-id="{{ block.id }}"
                  data-option-name="{{ option_name | handle }}"
                >
                  {% for value in option_values %}
                    <option value="{{ value }}">{{ value }}</option>
                  {% endfor %}
                </select>
                {% render 'icon-caret' %}
              </div>
            </div>
          {%- endif -%}
        {%- endif -%}
      {% endif %}
      {% break %}
    {% endif %}
  {% endfor %}
                          
{% endcase %}

Create new asset product-options-custom.js

document.addEventListener('DOMContentLoaded', function() {

  function handleCheckboxChange(checkbox) {
    const hiddenInputId = checkbox.dataset.hiddenInputId;
    const hiddenInput = document.getElementById(hiddenInputId);
    if (hiddenInput) {
      hiddenInput.value = checkbox.checked ? 'Yes' : 'No';
    }
  }

  function handleTextInputChange(input) {
    const hiddenInputId = input.dataset.hiddenInputId;
    const hiddenInput = document.getElementById(hiddenInputId);
    if (hiddenInput) {
      hiddenInput.value = input.value;
    }
  }

  function handleOptionInputChange(optionInputs) {
    optionInputs.forEach(input => {
      const productFormInput = document.getElementById(`product-form-input-${input.dataset.optionName}-${input.dataset.blockId}`);
      if (!productFormInput) {
        return;
      }

      function updateFields(value) {
        productFormInput.value = value;
      }

      input.addEventListener('change', function() {
        updateFields(this.value);
      });

      if (input.checked || input.tagName === 'SELECT') {
        updateFields(input.value);
      }
    });
  }

  const customCheckboxes = document.querySelectorAll('.custom-checkbox-input');
  customCheckboxes.forEach(checkbox => {
    checkbox.addEventListener('change', function() {
      handleCheckboxChange(checkbox);
    });
  });

  const customTextInputs = document.querySelectorAll('.custom-text-field');
  customTextInputs.forEach(input => {
    input.addEventListener('input', function() {
      handleTextInputChange(input);
    });
  });

  const optionInputs = document.querySelectorAll('input[name^="custom_option_"], select[name^="custom_option_"]');
  if (optionInputs.length > 0) {
    handleOptionInputChange(optionInputs);
  }
});

Create new asset product-options-custom.css

.custom-text-input {
  margin-bottom: 1rem;
}
.custom-text-input label {
  display: block;
  margin-bottom: 0.5rem;
}
.custom-text-field {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}

.custom-input-container {
  margin-bottom: 1rem;
}

.custom-checkbox-label {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}

.custom-checkbox-input {
  position: absolute;
  opacity: 0;
  height: 0;
  width: 0;
}

.custom-checkbox-slider {
  position: relative;
  display: inline-block;
  width: 40px;
  height: 20px;
  background-color: #ccc;
  border-radius: 20px;
  margin-right: 10px;
  transition: background-color 0.3s ease;
}

.custom-checkbox-slider::before {
  content: "";
  position: absolute;
  height: 16px;
  width: 16px;
  left: 2px;
  bottom: 2px;
  background-color: white;
  border-radius: 50%;
  transition: transform 0.3s ease;
}

.custom-checkbox-input:checked + .custom-checkbox-slider::before {
  transform: translateX(20px);
}

Edit buy-buttons.liquid

{%- for block in section.blocks -%}
  {% assign custom_input_properties = product.metafields.custom.custom_input_properties_to_enable | remove: '[' | remove: ']' | remove: '"' | split: ',' %}
  {% assign current_property = block.settings.property_name | strip %}
  {% for property in custom_input_properties %}
    {% assign trimmed_property = property | strip %}
    {% if trimmed_property == current_property %}
      {%- case block.type -%}
        {%- when 'custom_text_input' -%}
          <input 
            type="hidden"
            name="properties[{{ block.settings.property_name }}]" 
            id="hidden-{{ block.id }}-{{ block.settings.property_name | handle }}" 
            class="product-form-input custom-text-input"
            data-property-name="{{ block.settings.property_name }}"
          >
        {%- when 'custom_checkbox_input' -%}
            <input 
              type="hidden"
              name="properties[{{ block.settings.property_name }}]" 
              id="hidden-{{ block.id }}-{{ block.settings.property_name | handle }}"
              value="{% if block.settings.default_checked %}Yes{% else %}No{% endif %}"
            >
        {%- when 'custom_options_picker' -%}
          {% assign custom_options_pickers = product.metafields.custom.custom_options_pickers_to_enable.value %}
          {% assign matching_metaobject = nil %}
          {% for metaobject in custom_options_pickers %}
            {% if metaobject.option_name == current_property %}
              {% assign matching_metaobject = metaobject %}
              {% break %}
            {% endif %}
          {% endfor %}
          {% if matching_metaobject %}
            {% assign option_name = matching_metaobject.option_name %}
            {% assign option_values = matching_metaobject.option_values.value %}
            {% assign first_option_value = option_values | first %}
            <input 
              type="hidden"
              name="properties[{{ option_name }}]" 
              value="{{ first_option_value }}" 
              class="product-form-input line-item-prop"
              id="product-form-input-{{ option_name | handle }}-{{ block.id }}"
            >
          {% endif %}
      {%- endcase -%}
      {% break %}
    {% endif %}
  {% endfor %}
{%- endfor -%}

Edit email notifications

Order confirmation template. Update inside loop {% for line in subtotal_line_items %} and above the line {% if line.gift_card and line.properties["__shopify_send_gift_card_to_recipient"] %}

{% for property in line.properties %}
  {% if property.last == blank %}
    {% continue %}
  {% endif %}
  <span class="order-list__item-variant">{{ property.first }}: {{ property.last }}</span><br/>
{% endfor %}

Shipping confirmation template. Update inside loop {% for line in fulfillment.fulfillment_line_items %} and above the line {% if line.line_item.selling_plan_allocation %}

{% for property in line.line_item.properties %}
  {% if property.last == blank %}
    {% continue %}
  {% endif %}
  <span class="order-list__item-variant">{{ property.first }}: {{ property.last }}</span><br/>
{% endfor %}

 

 

Browse other ways to boost conversion rate & profit