Answers.Body: <p>I built something similar for a client recently (in Craft, but not Craft Commerce) that required two variations (size and colour) where not all variations were compatible (ie. some sizes were only available in some colours, and vice versa).</p>
  <p>The approach I took was to mimic Shopify’s approach. Each product has a table with three columns (size, colour, product code). I didn’t include price because it is always the same regardless of variations, but you could add that easily.</p>
  <p>On the page Craft outputs a single select input in case JavaScript fails for any reason. Something like:</p>
  <pre><code>{% if entry.options|length %}
  &lt;div class=&quot;product__options&quot;&gt;
    &lt;form class=&quot;#&quot; action=&quot;#&quot; method=&quot;post&quot;&gt;
      &lt;div class=&quot;form__wrapper&quot;&gt;
      
        &lt;fieldset class=&quot;form__group&quot;&gt;
          &lt;div class=&quot;select__wrapper&quot;&gt;
            &lt;label class=&quot;label&quot; for=&quot;opt__opt&quot;&gt;Options&lt;/label&gt;
            &lt;select class=&quot;input--select&quot; id=&quot;opt__opt&quot; name=&quot;options&quot;&gt;
              {% for row in entry.options %}
              &lt;option class=&quot;sel__option&quot; data-colour=&quot;{{ row.colour }}&quot; data-size=&quot;{{ row.size }}&quot; data-code=&quot;{{ row.code }}&quot; value=&quot;{{row.code}} - {{ row.size }} - {{ row.colour }}&quot;&gt;Size: {{ row.size }} | Colour: {{ row.colour }}&lt;/option&gt;
              {% endfor %}
            &lt;/select&gt;
          &lt;/div&gt;
        &lt;/fieldset&gt;
  
        &lt;fieldset class=&quot;form__group form__group--submit&quot;&gt;
          &lt;button class=&quot;button button--submit&quot; type=&quot;submit&quot; name=&quot;submit&quot; id=&quot;opt__btn&quot;&gt;Get Quick Quote&lt;/button&gt;
        &lt;/fieldset&gt;
      &lt;/div&gt;
    &lt;/form&gt;
  
  &lt;/div&gt;
  {% endif %}
  </code></pre>
  <p>Then, JavaScript lifts the data out of the select, and creates two new selects and a hidden input (the latter being for the product code). When the selects’ value changes the values in the other input are compared to make sure they are a valid combination (if not that option is disabled):</p>
  <pre><code>// holds the option data
  let option_data = [];
  
  // holds the inputs so can be manipulated
  let size_input,
      colour_input,
      code_input;
  
  // creates the wrapper for the select
  function createSelectWrapper(parent)
  {
    let sw = document.createElement(&quot;div&quot;);
    sw.setAttribute(&quot;class&quot;, &quot;select__wrapper&quot;);
    parent.appendChild(sw);
    return sw;
  }
  
  // creates the label for the select
  function createSelectLabel(parent, target, label)
  {
    // create wrapper
    let lw = document.createElement(&quot;div&quot;);
        lw.setAttribute(&quot;class&quot;, &quot;label__wrapper&quot;);
  
    // create label
    let lbl = document.createElement(&quot;label&quot;);
        lbl.setAttribute(&quot;class&quot;, &quot;label&quot;);
        lbl.setAttribute(&quot;for&quot;, target);
        lbl.innerHTML = label;
  
    lw.appendChild(lbl);
  
    parent.appendChild(lw);
  
  }
  
  
  // creates a select element
  function createSelect(parent, id, name, prompt, array)
  {
    // create select
    let sel = document.createElement(&quot;select&quot;);
  
    // set attributes
    sel.setAttribute(&quot;id&quot;, id);
    sel.setAttribute(&quot;name&quot;, name);
    sel.setAttribute(&quot;class&quot;, &quot;input--select&quot;);
  
    // create default
    let def = document.createElement(&quot;option&quot;);
    def.innerHTML = prompt;
    def.setAttribute(&quot;disabled&quot;, &quot;disabled&quot;);
    def.setAttribute(&quot;selected&quot;, &quot;selected&quot;);
    sel.appendChild(def);
  
    let l = array.length;
    for(let i = 0; i &lt; l; i++)
    {
      // create option
      let opt = document.createElement(&quot;option&quot;);
  
      // set attributes
      let val = array[i];
      opt.innerHTML = val;
      opt.setAttribute(&quot;value&quot;, val);
      opt.setAttribute(&quot;class&quot;, &quot;option&quot;);
  
      // append option
      sel.appendChild(opt);
    }
  
    // insert select
    parent.appendChild(sel);
  
    //return reference
    return sel;
  
  }
  
  
  // creates a hidden field to hold product code
  function createHidden(parent, id, name)
  {
  
    // create hidden input
    let inp = document.createElement(&quot;input&quot;);
  
    // set attributes
    inp.setAttribute(&quot;type&quot;, &quot;hidden&quot;);
    inp.setAttribute(&quot;id&quot;, id);
    inp.setAttribute(&quot;name&quot;, name);
  
    // add to page
    parent.appendChild(inp);
  
    // return reference
    return inp;
  
  }
  
  
  // checks if value is unique
  function getValueUnique(arr, val)
  {
    let i = arr.length,
        isFound = false;
    while(i--)
    {
      if(arr[i] == val)
      {
        isFound = true;
        break;
      }
    }
  
    return !isFound;
  }
  
  
  // toggles the enabled/disabled of the submit button
  function toggleSubmitBtn(willEnable)
  {
    let submitBtn = document.getElementById(&quot;opt__btn&quot;);
  
    if(willEnable)
    {
      submitBtn.removeAttribute(&quot;disabled&quot;);
    }
    else
    {
      submitBtn.setAttribute(&quot;disabled&quot;, &quot;disabled&quot;);
    }
  }
  
  // inits the options fields in the form
  function initOptions()
  {
  
    // grab the current input
    let orig_options = document.getElementById(&quot;opt__opt&quot;);
    let orig_options_parent = orig_options.parentNode;
    let orig_options_grandparent = orig_options_parent.parentNode;
    let orig_options_children = orig_options.getElementsByClassName(&quot;sel__option&quot;);
  
    // create colour Array
    let colour_arr = [];
  
    // create size Array
    let size_arr = [];
  
    // get the current data
    let l = orig_options_children.length;
    for(let i=0; i &lt; l; i++)
    {
      const t = orig_options_children[i].dataset;
      var json = JSON.stringify({
        colour: t.colour,
        size: t.size,
        code: t.code
      })
      option_data.push(json);
  
      // check if it needs to add to arrays
      if(getValueUnique(colour_arr, t.colour)) { colour_arr.push(t.colour); }
      if(getValueUnique(size_arr, t.size)) { size_arr.push(t.size); }
  
    }
  
    //sort arrays (just in case)
    colour_arr.sort();
    size_arr.sort(function(a, b) {return a - b});
  
    // remove the old dropdown
    orig_options_grandparent.removeChild(orig_options_parent);
  
    // create select wrapper
    let size_wrapper = createSelectWrapper(orig_options_grandparent);
    let colour_wrapper = createSelectWrapper(orig_options_grandparent);
  
    // add labels
    createSelectLabel(size_wrapper, &quot;opt__size&quot;, &quot;Size&quot;, true);
    createSelectLabel(colour_wrapper, &quot;opt__colour&quot;, &quot;Colour&quot;);
  
    // create new selects
    size_input = createSelect(size_wrapper, &quot;opt__size&quot;, &quot;size&quot;, &quot;Choose Size&quot;, size_arr);
    colour_input = createSelect(colour_wrapper, &quot;opt__colour&quot;, &quot;colour&quot;, &quot;Choose Colour&quot;, colour_arr);
  
    // create hidden field to hold product code
    code_input = createHidden(orig_options_grandparent, &quot;opt__code&quot;, &quot;product_code&quot;);
  
  
    // listen for changes on the options
    size_input.addEventListener(&quot;change&quot;, onOptionChange);
    colour_input.addEventListener(&quot;change&quot;, onOptionChange);
  
  }
  
  
  // handles the disabling of an option
  function disableOption(option, isSize)
  {
    option.setAttribute(&quot;disabled&quot;, &quot;disabled&quot;);
    option.innerHTML = option.getAttribute(&quot;value&quot;) + &quot; - not available in your chosen &quot; + (isSize ? &quot;colour&quot; : &quot;size&quot;);
  }
  
  // handles the enabling of an option
  function enableOption(option)
  {
    option.removeAttribute(&quot;disabled&quot;);
    option.innerHTML = option.getAttribute(&quot;value&quot;);
  }
  
  
  // event triggered by one of the option dropdowns changing
  function onOptionChange(event)
  {
  
    // identify selects
    let triggerSelect = event.target,
        targetSelect = triggerSelect == size_input ? colour_input : size_input;
  
    // first set the targetSelect to all be not available
    let targetOptions = targetSelect.getElementsByClassName(&quot;option&quot;);
    let l = targetOptions.length;
    for(let i = 0; i &lt; l; i++)
    {
      disableOption(targetOptions[i], targetSelect == size_input);
    }
  
    // next loop through the data to find the ones that should be turned back on
    let triggerProp = triggerSelect == size_input ? &quot;size&quot; : &quot;colour&quot;;
    let targetProp = triggerProp == &quot;size&quot; ? &quot;colour&quot; : &quot;size&quot;;
  
    let triggerVal = event.target.value;
    let availVals = [];
  
    l = option_data.length;
    for(i=0; i &lt; l; i++)
    {
      let jsonData = JSON.parse(option_data[i]);
  
      if(triggerVal == jsonData[triggerProp])
      {
        availVals.push(jsonData[targetProp]);
      }
  
    }
  
    // turn the target select options back on
    l = availVals.length;
    for(i = 0; i &lt; l; i++)
    {
      let m = targetOptions.length;
      for(let j = 0; j &lt; m; j++)
      {
        if(targetOptions[j].getAttribute(&quot;value&quot;) == availVals[i])
        {
          // found a match
          enableOption(targetOptions[j]);
        }
      }
    }
  
    // see if there’s a product code
    // (triggerVal is already set)
    targetVal = targetSelect.value;
  
    l = option_data.length;
    for(i = 0; i &lt; l; i++)
    {
      let jsonData = JSON.parse(option_data[i]);
      if(jsonData[triggerProp] == triggerVal &amp;&amp; jsonData[targetProp] == targetVal)
      {
        code_input.value = jsonData.code; // set product code in hidden input
        toggleSubmitBtn(true); // enable submit button
        break;
      }
    }
  
  }
  
  
  
  // inits page
  function init()
  {
    // disable button
    toggleSubmitBtn(false);
  
    // init options dropdown
    initOptions();
  
  }
  
  
  
  // check document is ready
  document.addEventListener('DOMContentLoaded', function(){
    init();
  })
  </code></pre>
  <p><em>(Some of that might look verbose, I’ve copied and pasted from a large file and tried to strip out anything irrelevant.)</em></p>
  <p>It’s not live yet, but it’s testing fine.</p>
  <p>I think for multi-variant products it’s the simplest approach for the client, because they can update the product in a simple table and it’s reasonably idiot-proof.</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2020-07-18T13:20:57.183Z
Answers.Text: Thanks for replying with that detail! We build sites in Shopify and that's exactly what I was hoping for is a similar way to manage variants.
Answers.UserDisplayName: JonnyT
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2020-07-18T13:28:52.683Z
Answers.Text: We ended up doing something similar to this, however, we created a single global that included all individual variants as their own blocks and used JS to create our own UI.
Answers.UserDisplayName: JonnyT
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2020-07-13T12:56:14.577Z
Answers.LastActivityDate: 2020-07-13T12:56:14.577Z
Answers.UserDisplayName: mitrol
Body: <p>I'm working on a Craft Commerce 3 project and for something I initially thought would be easy to do with variants seems to have completely thrown me off course so any feedback would be great.</p>
  <p>I have a site we're building that has one single product - a car registration plate maker. The user needs to input their reg and select from a few options such as styles, colours, badges etc. Some of these options change the price and some don't.</p>
  <p>I've built many sites in Craft CMS but only one previously with Craft Commerce 1. For this Craft Commerce site we built a plug-in that updates the product price and worked great so I'm thinking - do I need to go down the plug-in route or am I missing something with variants?</p>
  <p><strong>Variants</strong> - when I added variants, ideally I wanted a separate field for each variant option (text style, then one for size and then one for colours and so on) - not just one select field with the possible combinations.</p>
  <p><strong>Single Purchase Element</strong> - I've also been thinking just build the whole thing completely separate from Commerce so I'd in effect just have JS calculating the final price based on options selected. Then when added to cart, a form would just add the price, all the options selected and the custom text (car reg) to the line item.</p>
  <p>Any pointers on this would be really appreciated. I've tried many things but nothing seems to be a solid direction yet.</p>
  
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2020-07-14T09:36:53.137Z
Comments.Text: Have you seen this? https://www.vaersaagod.no/en/multi-attribute-variant-selection-in-craft-commerce
Comments.UserDisplayName: James Smith
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2020-07-18T13:16:37.923Z
Comments.Text: Thanks for that, I saw the article and followed it which was great but we have that many variants and possibilities, it just didn't seem manageable so we've created our own JS UI instead and plugin to change lineitem accordingly.
Comments.UserDisplayName: JonnyT
Tags: <ecommerce><plugin-craftcommerce3>
Title: Best approach for Craft Commerce 3 - Variants & Custom Text
ThreadText: # Best approach for Craft Commerce 3 - Variants & Custom Text
  
  (Asked by: username_0 on 2020-07-12)
  
  I'm working on a Craft Commerce 3 project and for something I initially thought would be easy to do with variants seems to have completely thrown me off course so any feedback would be great.
  
  I have a site we're building that has one single product - a car registration plate maker. The user needs to input their reg and select from a few options such as styles, colours, badges etc. Some of these options change the price and some don't.
  
  I've built many sites in Craft CMS but only one previously with Craft Commerce 1. For this Craft Commerce site we built a plug-in that updates the product price and worked great so I'm thinking - do I need to go down the plug-in route or am I missing something with variants?
  
  **Variants** - when I added variants, ideally I wanted a separate field for each variant option (text style, then one for size and then one for colours and so on) - not just one select field with the possible combinations.
  
  **Single Purchase Element** - I've also been thinking just build the whole thing completely separate from Commerce so I'd in effect just have JS calculating the final price based on options selected. Then when added to cart, a form would just add the price, all the options selected and the custom text (car reg) to the line item.
  
  Any pointers on this would be really appreciated. I've tried many things but nothing seems to be a solid direction yet.
  
  **Tags:** ecommerce, plugin-craftcommerce3
  
  > **Comments:**
  > **username_1**: Have you seen this? https://www.vaersaagod.no/en/multi-attribute-variant-selection-in-craft-commerce  
  > **username_0**: Thanks for that, I saw the article and followed it which was great but we have that many variants and possibilities, it just didn't seem manageable so we've created our own JS UI instead and plugin to change lineitem accordingly.  
  
  ---
  
  ## 1 answers
  
  ### Answer by username_2 on 2020-07-13. Score: 1 | Accepted answer
  
  I built something similar for a client recently (in Craft, but not Craft Commerce) that required two variations (size and colour) where not all variations were compatible (ie. some sizes were only available in some colours, and vice versa).
  
  The approach I took was to mimic Shopify’s approach. Each product has a table with three columns (size, colour, product code). I didn’t include price because it is always the same regardless of variations, but you could add that easily.
  
  On the page Craft outputs a single select input in case JavaScript fails for any reason. Something like:
  
  ```
  {% if entry.options|length %}
  <div class="product__options">
    <form class="#" action="#" method="post">
      <div class="form__wrapper">
      
        <fieldset class="form__group">
          <div class="select__wrapper">
            <label class="label" for="opt__opt">Options</label>
            <select class="input--select" id="opt__opt" name="options">
              {% for row in entry.options %}
              <option class="sel__option" data-colour="{{ row.colour }}" data-size="{{ row.size }}" data-code="{{ row.code }}" value="{{row.code}} - {{ row.size }} - {{ row.colour }}">Size: {{ row.size }} | Colour: {{ row.colour }}</option>
              {% endfor %}
            </select>
          </div>
        </fieldset>
  
        <fieldset class="form__group form__group--submit">
          <button class="button button--submit" type="submit" name="submit" id="opt__btn">Get Quick Quote</button>
        </fieldset>
      </div>
    </form>
  
  </div>
  {% endif %}
  
  ```
  
  Then, JavaScript lifts the data out of the select, and creates two new selects and a hidden input (the latter being for the product code). When the selects’ value changes the values in the other input are compared to make sure they are a valid combination (if not that option is disabled):
  
  ```
  // holds the option data
  let option_data = [];
  
  // holds the inputs so can be manipulated
  let size_input,
      colour_input,
      code_input;
  
  // creates the wrapper for the select
  function createSelectWrapper(parent)
  {
    let sw = document.createElement("div");
    sw.setAttribute("class", "select__wrapper");
    parent.appendChild(sw);
    return sw;
  }
  
  // creates the label for the select
  function createSelectLabel(parent, target, label)
  {
    // create wrapper
    let lw = document.createElement("div");
        lw.setAttribute("class", "label__wrapper");
  
    // create label
    let lbl = document.createElement("label");
        lbl.setAttribute("class", "label");
        lbl.setAttribute("for", target);
        lbl.innerHTML = label;
  
    lw.appendChild(lbl);
  
    parent.appendChild(lw);
  
  }
  
  // creates a select element
  function createSelect(parent, id, name, prompt, array)
  {
    // create select
    let sel = document.createElement("select");
  
    // set attributes
    sel.setAttribute("id", id);
    sel.setAttribute("name", name);
    sel.setAttribute("class", "input--select");
  
    // create default
    let def = document.createElement("option");
    def.innerHTML = prompt;
    def.setAttribute("disabled", "disabled");
    def.setAttribute("selected", "selected");
    sel.appendChild(def);
  
    let l = array.length;
    for(let i = 0; i < l; i++)
    {
      // create option
      let opt = document.createElement("option");
  
      // set attributes
      let val = array[i];
      opt.innerHTML = val;
      opt.setAttribute("value", val);
      opt.setAttribute("class", "option");
  
      // append option
      sel.appendChild(opt);
    }
  
    // insert select
    parent.appendChild(sel);
  
    //return reference
    return sel;
  
  }
  
  // creates a hidden field to hold product code
  function createHidden(parent, id, name)
  {
  
    // create hidden input
    let inp = document.createElement("input");
  
    // set attributes
    inp.setAttribute("type", "hidden");
    inp.setAttribute("id", id);
    inp.setAttribute("name", name);
  
    // add to page
    parent.appendChild(inp);
  
    // return reference
    return inp;
  
  }
  
  // checks if value is unique
  function getValueUnique(arr, val)
  {
    let i = arr.length,
        isFound = false;
    while(i--)
    {
      if(arr[i] == val)
      {
        isFound = true;
        break;
      }
    }
  
    return !isFound;
  }
  
  // toggles the enabled/disabled of the submit button
  function toggleSubmitBtn(willEnable)
  {
    let submitBtn = document.getElementById("opt__btn");
  
    if(willEnable)
    {
      submitBtn.removeAttribute("disabled");
    }
    else
    {
      submitBtn.setAttribute("disabled", "disabled");
    }
  }
  
  // inits the options fields in the form
  function initOptions()
  {
  
    // grab the current input
    let orig_options = document.getElementById("opt__opt");
    let orig_options_parent = orig_options.parentNode;
    let orig_options_grandparent = orig_options_parent.parentNode;
    let orig_options_children = orig_options.getElementsByClassName("sel__option");
  
    // create colour Array
    let colour_arr = [];
  
    // create size Array
    let size_arr = [];
  
    // get the current data
    let l = orig_options_children.length;
    for(let i=0; i < l; i++)
    {
      const t = orig_options_children[i].dataset;
      var json = JSON.stringify({
        colour: t.colour,
        size: t.size,
        code: t.code
      })
      option_data.push(json);
  
      // check if it needs to add to arrays
      if(getValueUnique(colour_arr, t.colour)) { colour_arr.push(t.colour); }
      if(getValueUnique(size_arr, t.size)) { size_arr.push(t.size); }
  
    }
  
    //sort arrays (just in case)
    colour_arr.sort();
    size_arr.sort(function(a, b) {return a - b});
  
    // remove the old dropdown
    orig_options_grandparent.removeChild(orig_options_parent);
  
    // create select wrapper
    let size_wrapper = createSelectWrapper(orig_options_grandparent);
    let colour_wrapper = createSelectWrapper(orig_options_grandparent);
  
    // add labels
    createSelectLabel(size_wrapper, "opt__size", "Size", true);
    createSelectLabel(colour_wrapper, "opt__colour", "Colour");
  
    // create new selects
    size_input = createSelect(size_wrapper, "opt__size", "size", "Choose Size", size_arr);
    colour_input = createSelect(colour_wrapper, "opt__colour", "colour", "Choose Colour", colour_arr);
  
    // create hidden field to hold product code
    code_input = createHidden(orig_options_grandparent, "opt__code", "product_code");
  
    // listen for changes on the options
    size_input.addEventListener("change", onOptionChange);
    colour_input.addEventListener("change", onOptionChange);
  
  }
  
  // handles the disabling of an option
  function disableOption(option, isSize)
  {
    option.setAttribute("disabled", "disabled");
    option.innerHTML = option.getAttribute("value") + " - not available in your chosen " + (isSize ? "colour" : "size");
  }
  
  // handles the enabling of an option
  function enableOption(option)
  {
    option.removeAttribute("disabled");
    option.innerHTML = option.getAttribute("value");
  }
  
  // event triggered by one of the option dropdowns changing
  function onOptionChange(event)
  {
  
    // identify selects
    let triggerSelect = event.target,
        targetSelect = triggerSelect == size_input ? colour_input : size_input;
  
    // first set the targetSelect to all be not available
    let targetOptions = targetSelect.getElementsByClassName("option");
    let l = targetOptions.length;
    for(let i = 0; i < l; i++)
    {
      disableOption(targetOptions[i], targetSelect == size_input);
    }
  
    // next loop through the data to find the ones that should be turned back on
    let triggerProp = triggerSelect == size_input ? "size" : "colour";
    let targetProp = triggerProp == "size" ? "colour" : "size";
  
    let triggerVal = event.target.value;
    let availVals = [];
  
    l = option_data.length;
    for(i=0; i < l; i++)
    {
      let jsonData = JSON.parse(option_data[i]);
  
      if(triggerVal == jsonData[triggerProp])
      {
        availVals.push(jsonData[targetProp]);
      }
  
    }
  
    // turn the target select options back on
    l = availVals.length;
    for(i = 0; i < l; i++)
    {
      let m = targetOptions.length;
      for(let j = 0; j < m; j++)
      {
        if(targetOptions[j].getAttribute("value") == availVals[i])
        {
          // found a match
          enableOption(targetOptions[j]);
        }
      }
    }
  
    // see if there’s a product code
    // (triggerVal is already set)
    targetVal = targetSelect.value;
  
    l = option_data.length;
    for(i = 0; i < l; i++)
    {
      let jsonData = JSON.parse(option_data[i]);
      if(jsonData[triggerProp] == triggerVal && jsonData[targetProp] == targetVal)
      {
        code_input.value = jsonData.code; // set product code in hidden input
        toggleSubmitBtn(true); // enable submit button
        break;
      }
    }
  
  }
  
  // inits page
  function init()
  {
    // disable button
    toggleSubmitBtn(false);
  
    // init options dropdown
    initOptions();
  
  }
  
  // check document is ready
  document.addEventListener('DOMContentLoaded', function(){
    init();
  })
  
  ```
  
  *(Some of that might look verbose, I’ve copied and pasted from a large file and tried to strip out anything irrelevant.)*
  
  It’s not live yet, but it’s testing fine.
  
  I think for multi-variant products it’s the simplest approach for the client, because they can update the product in a simple table and it’s reasonably idiot-proof.
  
  > **Comments:**
  > **username_0**: Thanks for replying with that detail! We build sites in Shopify and that's exactly what I was hoping for is a similar way to manage variants.  
  > **username_0**: We ended up doing something similar to this, however, we created a single global that included all individual variants as their own blocks and used JS to create our own UI.  
  
  ---
  
  