Nesting optgroups in a dropdownlist/select

HtmlOptgroup

Html Problem Overview


I have created a customer c# DropDownList control that can render out it's contents are optgroups (Not from scratch, I edited some code found on the internet, although I do understand exactly what it's doing), and it works fine.

However, I have now come across a situation where I need to have two levels of indentation in my dropdown, i.e.

<select>
  <optgroup label="Level One">
    <option> A.1 </option>
    <optgroup label="Level Two">
      <option> A.B.1 </option>
    </optgroup>
    <option> A.2 </option>
  </optgroup>
</select>

However, in the example snippet above, it is rendering as if Level Two was at the same amount of indentation as Level One.

Is there a way to produce the nested optgroup behavior I am looking for?

Html Solutions


Solution 1 - Html

The HTML spec here is really broken. It should allow nested optgroups and recommend user agents render them as nested menus. Instead, only one optgroup level is allowed. However, they do have to say the following on the subject:

> Note. Implementors are advised that future versions of HTML may extend the grouping mechanism to allow for nested groups (i.e., OPTGROUP elements may nest). This will allow authors to represent a richer hierarchy of choices.

And user agents could start using submenus to render optgoups instead of displaying titles before the first option element in an optgroup as they do now.

Solution 2 - Html

This is just fine but if you add option which is not in optgroup it gets buggy.

<select>
  <optgroup label="Level One">
    <option> A.1 </option>
    <optgroup label="&nbsp;&nbsp;&nbsp;&nbsp;Level Two">
      <option>&nbsp;&nbsp;&nbsp;&nbsp; A.B.1 </option>
    </optgroup>
    <option> A.2 </option>
  </optgroup>
  <option> A </option>
</select>

Would be much better if you used css and close optgroup right away :

<select>
  <optgroup label="Level One"></optgroup>
  <option style="padding-left:15px"> A.1 </option>
  <optgroup label="Level Two" style="padding-left:15px"></optgroup>
  <option style="padding-left:30px"> A.B.1 </option>
  <option style="padding-left:15px"> A.2 </option>
  <option> A </option>
</select>

Solution 3 - Html

Ok, if anyone ever reads this: the best option is to add four &nbsp;s at each extra level of indentation, it would seem!

so:

<select>
 <optgroup label="Level One">
  <option> A.1 </option>
  <optgroup label="&nbsp;&nbsp;&nbsp;&nbsp;Level Two">
   <option>&nbsp;&nbsp;&nbsp;&nbsp; A.B.1 </option>
  </optgroup>
  <option> A.2 </option>
 </optgroup>
</select>

Solution 4 - Html

<style>
  .NestedSelect{display: inline-block; height: 100px; border: 1px Black solid; overflow-y: scroll;}
  .NestedSelect label{display: block; cursor: pointer;}
  .NestedSelect label:hover{background-color: #0092ff; color: White;}
  .NestedSelect input[type="radio"]{display: none;}
  .NestedSelect input[type="radio"] + span{display: block; padding-left: 0px; padding-right: 5px;}
  .NestedSelect input[type="radio"]:checked + span{background-color: Black; color: White;}
  .NestedSelect div{margin-left: 15px; border-left: 1px Black solid;}
  .NestedSelect label > span:before{content: '- ';}
</style>

<div class="NestedSelect">
  <label><input type="radio" name="MySelectInputName"><span>Fruit</span></label>
  <div>
    <label><input type="radio" name="MySelectInputName"><span>Apple</span></label>
    <label><input type="radio" name="MySelectInputName"><span>Banana</span></label>
    <label><input type="radio" name="MySelectInputName"><span>Orange</span></label>
  </div>

  <label><input type="radio" name="MySelectInputName"><span>Drink</span></label>
  <div>
    <label><input type="radio" name="MySelectInputName"><span>Water</span></label>

    <label><input type="radio" name="MySelectInputName"><span>Soft</span></label>
    <div>
      <label><input type="radio" name="MySelectInputName"><span>Cola</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Soda</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Lemonade</span></label>
    </div>

    <label><input type="radio" name="MySelectInputName"><span>Hard</span></label>
    <div>
      <label><input type="radio" name="MySelectInputName"><span>Bear</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Whisky</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Vodka</span></label>
      <label><input type="radio" name="MySelectInputName"><span>Gin</span></label>
    </div>
  </div>
</div>

Solution 5 - Html

I really like the Broken Arrow's solution above in this post. I have just improved/changed it a bit so that what was called labels can be toggled and are not considered options. I have used a small piece of jQuery, but this could be done without jQuery.

I have replaced intermediate labels (no leaf labels) with links, which call a function on click. This function is in charge of toggling the next div of the clicked link, so that it expands/collapses the options. This avoids the possibility of selecting an intermediate element in the hierarchy, which usually is something desired. Making a variant that allows to select intermediate elements should be easy.

This is the modified html:

<div class="NestedSelect">
    <a onclick="toggleDiv(this)">Fruit</a>
    <div>
        <label>
            <input type="radio" name="MySelectInputName"><span>Apple</span></label>
        <label>
            <input type="radio" name="MySelectInputName"><span>Banana</span></label>
        <label>
            <input type="radio" name="MySelectInputName"><span>Orange</span></label>
    </div>

    <a onclick="toggleDiv(this)">Drink</a>
    <div>
        <label>
            <input type="radio" name="MySelectInputName"><span>Water</span></label>

        <a onclick="toggleDiv(this)">Soft</a>
        <div>
            <label>
                <input type="radio" name="MySelectInputName"><span>Cola</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Soda</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Lemonade</span></label>
        </div>

        <a onclick="toggleDiv(this)">Hard</a>
        <div>
            <label>
                <input type="radio" name="MySelectInputName"><span>Bear</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Whisky</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Vodka</span></label>
            <label>
                <input type="radio" name="MySelectInputName"><span>Gin</span></label>
        </div>
    </div>
</div>

A small javascript/jQuery function:

function toggleDiv(element) {
    $(element).next('div').toggle('medium');
}

And the css:

.NestedSelect {
    display: inline-block;
    height: 100%;
    border: 1px Black solid;
    overflow-y: scroll;
}

.NestedSelect a:hover, .NestedSelect span:hover  {
    background-color: #0092ff;
    color: White;
    cursor: pointer;
}

.NestedSelect input[type="radio"] {
    display: none;
}

.NestedSelect input[type="radio"] + span {
    display: block;
    padding-left: 0px;
    padding-right: 5px;
}

.NestedSelect input[type="radio"]:checked + span {
    background-color: Black;
    color: White;
}

.NestedSelect div {
    display: none;
    margin-left: 15px;
    border-left: 1px black
    solid;
}

.NestedSelect label > span:before, .NestedSelect a:before{
    content: '- ';
}

.NestedSelect a {
    display: block;
}

Running sample in JSFiddle

Solution 6 - Html

I think if you have something that structured and complex, you might consider something other than a single drop-down box.

Solution 7 - Html

I know this was quite a while ago, however I have a little extra to add:

This is not possible in HTML5 or any previous specs, nor is it proposed in HTML5.1 yet. I have made a request to the public-html-comments mailing list, but we'll see if anything comes of it.

Regardless, whilst this is not possible using <select> yet, you can achieve a similar effect with the following HTML, plus some CSS for prettiness:

<ul>
  <li>
	<input type="radio" name="location" value="0" id="loc_0" />
	<label for="loc_0">United States</label>
	<ul>
	  <li>
		Northeast
        <ul>
	      <li>
			<input type="radio" name="location" value="1" id="loc_1" />
			<label for="loc_1">New Hampshire</label>
		  </li>
          <li>
			<input type="radio" name="location" value="2" id="loc_2" />
			<label for="loc_2">Vermont</label>
		  </li>
          <li>
			 <input type="radio" name="location" value="3" id="loc_3" />
			 <label for="loc_3">Maine</label>
		  </li>
		 </ul>
	   </li>
       <li>
		   Southeast
           <ul>
			 <li>
				<input type="radio" name="location" value="4" id="loc_4" />
				<label for="loc_4">Georgia</label>
			 </li>
             <li>
				<input type="radio" name="location" value="5" id="loc_5" />
				<label for="loc_5">Alabama</label>
			 </li>
		   </ul>
		</li>
	  </ul>
    </li>
    <li>
	   <input type="radio" name="location" value="6" id="loc_6" />
	   <label for="loc_6">Canada</label>
       <ul>
		  <li>
			 <input type="radio" name="location" value="7" id="loc_7" />
			 <label for="loc_7">Ontario</label>
		  </li>
          <li>
			  <input type="radio" name="location" value="8" id="loc_8" />
		      <label for="loc_8">Quebec</label>
		  </li>
          <li>
			   <input type="radio" name="location" value="9" id="loc_9" />
			   <label for="loc_9">Manitoba</label>
		  </li>
		</ul>
	 </li>
  </ul>

As an extra added benefit, this also means you can allow selection of the <optgroups> themselves. This might be useful if you had, for example, nested categories where the categories go into heavy detail and you want to allow users to select higher up in the hierarchy.

This will all work without JavaScript, however you might wish to add some to hide the radio buttons and then change the background color of the selected item or something.

Bear in mind, this is far from a perfect solution, but if you absolutely need a nested select with reasonable cross-browser compatibility, this is probably as close as you're going to get.

Solution 8 - Html

I needed clean and lightweight solution (so no jQuery and alike), which will look exactly like plain HTML, would also continue working when only plain HTML is preset (so javascript will only enhance it), and which will allow searching by starting letters (including national UTF-8 letters) if possible where it does not add extra weight. It also must work fast on very slow browsers (think rPi - so preferably no javascript executing after page load).

In firefox it uses CSS identing and thus allow searching by letters, and in other browsers it will use &nbsp; prepending (but there it does not support quick search by letters). Anyway, I'm quite happy with results.

You can try it in action here

It goes like this:

CSS:

.i0 { }
.i1 { margin-left: 1em; }
.i2 { margin-left: 2em; }
.i3 { margin-left: 3em; }
.i4 { margin-left: 4em; }
.i5 { margin-left: 5em; }

HTML (class "i1", "i2" etc denote identation level):

<form action="/filter/" method="get">
<select name="gdje" id="gdje">
<option value=1 class="i0">Svugdje</option>
<option value=177 class="i1">Bosna i Hercegovina</option>
<option value=190 class="i2">Babin Do</option>  
<option value=258 class="i2">Banja Luka</option>
<option value=181 class="i2">Tuzla</option>
<option value=307 class="i1">Crna Gora</option>
<option value=308 class="i2">Podgorica</option>
<option value=2 SELECTED class="i1">Hrvatska</option>
<option value=5 class="i2">Bjelovarsko-bilogorska županija</option>
<option value=147 class="i3">Bjelovar</option>
<option value=79 class="i3">Daruvar</option>  
<option value=94 class="i3">Garešnica</option>
<option value=329 class="i3">Grubišno Polje</option>
<option value=368 class="i3">Čazma</option>
<option value=6 class="i2">Brodsko-posavska županija</option>
<option value=342 class="i3">Gornji Bogićevci</option>
<option value=158 class="i3">Klakar</option>
<option value=140 class="i3">Nova Gradiška</option>
</select>
</form>

<script>
<!--
        window.onload = loadFilter;
// -->   
</script>

JavaScript:

function loadFilter() {
  'use strict';
  // indents all options depending on "i" CSS class
  function add_nbsp() {
    var opt = document.getElementsByTagName("option");
    for (var i = 0; i < opt.length; i++) {
      if (opt[i].className[0] === 'i') {
      opt[i].innerHTML = Array(3*opt[i].className[1]+1).join("&nbsp;") + opt[i].innerHTML;      // this means "&nbsp;" x (3*$indent)
      }
    }
  }
  // detects browser
  navigator.sayswho= (function() {
    var ua= navigator.userAgent, tem,
    M= ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*([\d\.]+)/i) || [];
    if(/trident/i.test(M[1])){
        tem=  /\brv[ :]+(\d+(\.\d+)?)/g.exec(ua) || [];
        return 'IE '+(tem[1] || '');
    }
    M= M[2]? [M[1], M[2]]:[navigator.appName, navigator.appVersion, '-?'];
    if((tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
    return M.join(' ');
  })();
  // quick detection if browser is firefox
  function isFirefox() {
    var ua= navigator.userAgent,
    M= ua.match(/firefox\//i);  
    return M;
  }
  // indented select options support for non-firefox browsers
  if (!isFirefox()) {
    add_nbsp();
  }
}  

Solution 9 - Html

I have written a beautiful, nested select. Maybe it will help you.

https://jsfiddle.net/nomorepls/tg13w5r7/1/

function on_change_select(e) {
  alert(e.value, e.title, e.option, e.select);
}

$(document).ready(() => {
  // NESTED SELECT

  $(document).on('click', '.nested-cell', function() {
    $(this).next('div').toggle('medium');
  });

  $(document).on('change', 'input[name="nested-select-hidden-radio"]', function() {
    const parent = $(this).closest(".nested-select");
    const value = $(this).attr('value');
    const title = $(this).attr('title');
    const executer = parent.attr('executer');
    if (executer) {
      const event = new Object();
      event.value = value;
      event.title = title;
      event.option = $(this);
      event.select = parent;
      window[executer].apply(null, [event]);
    }
    parent.attr('value', value);
    parent.parent().slideToggle();
    const button = parent.parent().prev();
    button.toggleClass('active');
    button.addClass('selected');
    button.children('.nested-select-title').html(title);
  });

  $(document).on('click', '.nested-select-button', function() {
    const button = $(this);
    let select = button.parent().children('.nested-select-wrapper');

    if (!button.hasClass('active')) {
      select = select.detach();
      if (button.height() + button.offset().top + $(window).height() * 0.4 > $(window).height()) {
        select.insertBefore(button);
        select.css('margin-top', '-44vh');
        select.css('top', '0');
      } else {
        select.insertAfter(button);
        select.css('margin-top', '');
        select.css('top', '40px');
      }
    }
    select.slideToggle();
    button.toggleClass('active');
  });
});

.container {
  width: 200px;
  position: relative;
  top: 0;
  left: 0;
  right: 0;
  height: auto;
}

.nested-select-box {
  font-family: Arial, Helvetica, sans-serif;
  display: block;
  position: relative;
  width: 100%;
  height: fit-content;
  cursor: pointer;
  color: #2196f3;
  height: 40px;
  font-size: small;
  /* z-index: 2000; */
}

.nested-select-box .nested-select-button {
  border: 1px solid #2196f3;
  position: absolute;
  width: calc(100% - 20px);
  padding: 0 10px;
  min-height: 40px;
  word-wrap: break-word;
  margin: 0 auto;
  overflow: hidden;
}

.nested-select-box.danger .nested-select-button {
  border: 1px solid rgba(250, 33, 33, 0.678);
}

.nested-select-box .nested-select-button .nested-select-title {
  padding-right: 25px;
  padding-left: 25px;
  width: calc(100% - 50px);
  margin: auto;
  height: fit-content;
  text-align: center;
  vertical-align: middle;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
}

.nested-select-box .nested-select-button.selected .nested-select-title {
  bottom: unset;
  top: 5px;
}

.nested-select-box .nested-select-button .nested-select-title-icon {
  position: absolute;
  height: 20px;
  width: 20px;
  top: 10px;
  bottom: 10px;
  right: 7px;
  transition: all 0.5s ease 0s;
}

.nested-select-box .nested-select-button.active .nested-select-title-icon {
  -moz-transform: scale(-1, -1);
  -o-transform: scale(-1, -1);
  -webkit-transform: scale(-1, -1);
  transform: scale(-1, -1);
}

.nested-select-box .nested-select-button .nested-select-title-icon::before,
.nested-select-box .nested-select-button .nested-select-title-icon::after {
  content: "";
  background-color: #2196f3;
  position: absolute;
  width: 70%;
  height: 2px;
  transition: all 0.5s ease 0s;
  top: 9px;
}

.nested-select-box .nested-select-button .nested-select-title-icon::before {
  transform: rotate(45deg);
  left: -1.6px;
}

.nested-select-box .nested-select-button .nested-select-title-icon::after {
  transform: rotate(-45deg);
  left: 7px;
}

.nested-select-box .nested-select-wrapper {
  width: 100%;
  top: 40px;
  position: relative;
  border: 1px solid #2196f3;
  background: #ffffff;
  z-index: 2005;
  opacity: 1;
}

.nested-select {
  font-family: Arial, Helvetica, sans-serif;
  display: inline-block;
  overflow-y: scroll;
  max-height: 40vh;
  width: calc(100% - 10px);
  padding: 5px;
  -ms-overflow-style: none;
  scrollbar-width: none;
}

.nested-select::-webkit-scrollbar {
  display: none;
}

.nested-select a,
.nested-select span {
  padding: 0 5px;
  border-radius: 3px;
  cursor: pointer;
  text-align: start;
}

.nested-select a:hover {
  background-color: #62b2f3;
  color: #ffffff;
}

.nested-select span:hover {
  background-color: #c4c4c4;
  color: #ffffff;
}

.nested-select input[type="radio"] {
  display: none;
}

.nested-select input[type="radio"]+span {
  display: block;
}

.nested-select input[type="radio"]:checked+span {
  background-color: #2196f3;
  color: #ffffff;
}

.nested-select div {
  margin-left: 15px;
}

.nested-select label>span:before,
.nested-select a:before {
  content: "\2022";
  margin-right: 5px;
}

.nested-select a {
  display: block;
}

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
  <div class="nested-select-box w-100">
    <div class="nested-select-button">
      <p class="nested-select-title">
        Account
      </p>
      <span class="nested-select-title-icon"></span>
    </div>
    <div class="nested-select-wrapper" style="display: none;">
      <div class="nested-select" executer="on_change_select">

        <label>
        <input title="Accounting and legal services" value="1565142000000891539" type="radio" name="nested-select-hidden-radio">
        <span>Accounting and legal services</span>
      </label>



        <label>
        <input title="Advertising agencies" value="1565142000000891341" type="radio" name="nested-select-hidden-radio">
        <span>Advertising agencies</span>
      </label>



        <a class="nested-cell">Advertising And Marketing</a>
        <div>



          <label>
          <input title="Advertising agencies" value="1565142000000891341" type="radio" name="nested-select-hidden-radio">
          <span>Advertising agencies</span>
        </label>



          <a class="nested-cell">Adwords - traffic</a>
          <div>



            <label>
            <input title="Adwords - traffic: Charters and general search" value="1565142000003929177" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Charters and general search</span>
          </label>



            <label>
            <input title="Adwords - traffic: Distance course" value="1565142000007821291" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Distance course</span>
          </label>



            <label>
            <input title="Adwords - traffic: Events" value="1565142000003929189" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Events</span>
          </label>



            <label>
            <input title="Adwords - traffic: Practices" value="1565142000003929165" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Practices</span>
          </label>



            <label>
            <input title="Adwords - traffic: Sailing tours" value="1565142000003929183" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Sailing tours</span>
          </label>



            <label>
            <input title="Adwords - traffic: Theoretical courses" value="1565142000003929171" type="radio" name="nested-select-hidden-radio">
            <span>Adwords - traffic: Theoretical courses</span>
          </label>



          </div>



          <label>
          <input title="Branded products" value="1565142000000891533" type="radio" name="nested-select-hidden-radio">
          <span>Branded products</span>
        </label>



          <label>
          <input title="Business cards" value="1565142000005438323" type="radio" name="nested-select-hidden-radio">
          <span>Business cards</span>
        </label>



          <a class="nested-cell">Facebook, Instagram - traffic</a>
          <div>



            <label>
            <input title="Facebook, Instagram - traffic: Charters and general search" value="1565142000003929145" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Charters and general search</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Distance course" value="1565142000007821285" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Distance course</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Events" value="1565142000003929157" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Events</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Practices" value="1565142000003929133" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Practices</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Sailing tours" value="1565142000003929151" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Sailing tours</span>
          </label>



            <label>
            <input title="Facebook, Instagram - traffic: Theoretical courses" value="1565142000003929139" type="radio" name="nested-select-hidden-radio">
            <span>Facebook, Instagram - traffic: Theoretical courses</span>
          </label>



          </div>



          <label>
          <input title="Offline Advertising (posters, banners, partnerships)" value="1565142000000891377" type="radio" name="nested-select-hidden-radio">
          <span>Offline Advertising (posters, banners, partnerships)</span>
        </label>



          <label>
          <input title="Photos, video etc." value="1565142000000891371" type="radio" name="nested-select-hidden-radio">
          <span>Photos, video etc.</span>
        </label>



          <label>
          <input title="Prize fund" value="1565142000001404931" type="radio" name="nested-select-hidden-radio">
          <span>Prize fund</span>
        </label>



          <label>
          <input title="SEO" value="1565142000000891365" type="radio" name="nested-select-hidden-radio">
          <span>SEO</span>
        </label>



          <label>
          <input title="SMM Content creation (texts, copywriting)" value="1565142000000891389" type="radio" name="nested-select-hidden-radio">
          <span>SMM Content creation (texts, copywriting)</span>
        </label>



          <a class="nested-cell">YouTube</a>
          <div>



            <label>
            <input title="YouTube: travel expenses" value="1565142000008100163" type="radio" name="nested-select-hidden-radio">
            <span>YouTube: travel expenses</span>
          </label>



            <label>
            <input title="Youtube: video editing" value="1565142000008100157" type="radio" name="nested-select-hidden-radio">
            <span>Youtube: video editing</span>
          </label>



          </div>



        </div>

      </div>
    </div>
  </div>
</div>

Solution 10 - Html

It seems that the initial inventors of the standard want a group-based selection list rather than a hierarchical list. And, unfortunately, future HTML standard probably won't change this due to a high risk of breaking downward compatibility.

When acceptable, converting the hierarchical list into a segment-named group list may be a workaround:

<select>
  <optgroup label="Level One">
    <option> A.1 </option>
  </optgroup>
  <optgroup label="Level One / Level Two">
    <option> A.B.1 </option>
  </optgroup>
  <optgroup label="Level One">
    <option> A.2 </option>
  </optgroup>
</select>

If a change of order is acceptable, consider merging items with identical group name for better look:

<select>
  <optgroup label="Level One">
    <option> A.1 </option>
    <option> A.2 </option>
  </optgroup>
  <optgroup label="Level One / Level Two">
    <option> A.B.1 </option>
  </optgroup>
</select>

If both are not acceptable and a hierarchical selection list is really required, implementation of a modal dialog with checkboxes (and possibly some JavaScript keyboard trap for multi-selection with Shift or Ctrl key) may be needed.

Solution 11 - Html

Nested Accordeon select

I made this approach since I couldn´t find what I was searching. A nested accordeon select. Its CSS is very simple and can be improved. The only thing you need is an object with keys and values you want to add into the select. Keys would be subgroups, and key values (arrays and single elements) would be selectable items.

Once you have your array, only thing you need to do is to call

initAccordeon(obj);

with your data object as an argument, and the nested accordeon will appear:

const obj = {
  Cars: {
    SwedishCars: [
      "Volvo",
      "Saab"
    ],
    GermanCars: [
      "Mercedes",
      {
        Audi: [
          "Audi A3", 
          "Audi A4", 
          "Audi A5" 
        ]
      }
    ]
  },
  Food: {
    Fruits: [
      "Orange",
      "Apple",
      "Banana"
    ],
    SaltyFoods: [
      "Pretzels",
      "Burger",
      "Noodles"
    ],
    Drinks: "Water"
  }
};

initAccordeon(obj);   // <--------------------------- Call initialization

function accordeonAddEvents() {
  Array.from(document.getElementsByClassName("accordeon-header")).forEach(function(header) {
    if (header.getAttribute("listener") !== "true") {
      header.addEventListener("click", function() {
        this.parentNode.getElementsByClassName("accordeon-body")[0].classList.toggle("hide");
      });
      header.setAttribute("listener", "true");
    }
  });

  Array.from(document.getElementsByClassName("button-group")).forEach(function(but) {
    if (but.getAttribute("listener") !== "true") {
      but.addEventListener("click", function() {
        if (this.getAttribute("depth") === "-1") {
          let header = this;
          while ((header = header.parentElement) && header.className !== "accordeon");
          header.getElementsByClassName("accordeon-header")[0].innerHTML = this.innerHTML;
          return;
        }
        const groups = Array.from(this.parentNode.getElementsByClassName("accordeon-group"));
        groups.forEach(g => {
          if (g.getAttribute("uuid") === this.getAttribute("uuid") &&
            g.getAttribute("depth") === this.getAttribute("depth")) {
            g.classList.toggle("hide");
          }
        });
      });
      but.setAttribute("listener", "true");
    }
  });
}

function initAccordeon(data) {
  accordeons = Array.from(document.getElementsByClassName("accordeon-body"));
  accordeons.forEach(acc => {
    acc.innerHTML = "";
    const route = (subObj, keyIndex = 0, parent = acc, depth = 0) => {
      const keys = Object.keys(subObj);
      if (typeof subObj === 'object' && !Array.isArray(subObj) && keys.length > 0) {
        while (keyIndex < keys.length) {
          var but = document.createElement("button");
          but.className = "button-group";
          but.setAttribute("uuid", keyIndex);
          but.setAttribute("depth", depth);
          but.innerHTML = keys[keyIndex];
          var group = document.createElement("div");
          group.className = "accordeon-group hide";
          group.setAttribute("uuid", keyIndex);
          group.setAttribute("depth", depth);
          route(subObj[keys[keyIndex]], 0, group, depth + 1);
          keyIndex++;
          parent.append(but);
          parent.append(group);
        }
      } else {
        if (!Array.isArray(subObj)) subObj = [subObj];
        subObj.forEach((e, i) => {
          if (typeof e === 'object') {
              route(e, 0, parent, depth);
          } else {
              var but = document.createElement("button");
              but.className = "button-group";
              but.setAttribute("uuid", i);
              but.setAttribute("depth", "-1");
              but.innerHTML = e;
              parent.append(but);
          }
        });
      }
    };
    route(data);
  });
  accordeonAddEvents();
}

.accordeon {
  width: 460px;
  height: auto;
  min-height: 340px;
  font-size: 20px;
  cursor: pointer;
  user-select: none;
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  -o-user-select: none;
  display: block;
  position: relative;
  z-index: 10;
}

.accordeon-header {
  display: inline-block;
  width: 450px;
  border: solid 0.1vw black;
  border-radius: 0.2vw;
  background-color: white;
  padding-left: 10px;
  color: black;
}

.accordeon-header:hover {
  opacity: 0.7;
}

.accordeon-body {
  display: block;
  position: absolute;
}

.button-group {
  display: block;
  cursor: pointer;
  width: 460px;
  text-align: left;
  font-size: 20px;
  font-weight: bold;
}

.accordeon-group {
  padding-left: 20px;
}

.accordeon-group .button-group {
  width: 100%;
}

.button-group[depth="-1"] {
  color: green;
}

.hide {
  display: none;
}

<div class="accordeon">
  <span class="accordeon-header">Select something</span>
  <div class="accordeon-body hide">
      
  </div>
</div>

Solution 12 - Html

As of the end of 2021 there is still no nesting of <optgroup>.

I've made a simple SCSS, which requires no JS at all. For <select size> and <select multiple> to align option based on <option class="depth-[0-100]">.

Only differs at <optgroup> which should not be used anymore, instead of them use <option class="depth-n" disabled label="..." />

<select size="10">
  <!-- Behaves similar to optgroup if disabled -->
  <option class="depth-0" label="Root" disabled />
  
  <!-- Examples -->
  <option class="depth-1">1</option>
  <option class="depth-2" value="something">1.2</option>
  <option class="depth-20" value="Level 20">20</option>
  
  <option class="depth-0" label="Next item on Root" disabled />
  <option class="depth-1" label="Sub" disabled />
  <option class="depth-2" value="#fff">White</option>
</select>
/* can easily be adjusted to support even more */
@for $i from 0 through 100 { 
  select[size] option.depth-#{$i},
  select[multiple] option.depth-#{$i} {
      padding-left: calc(0.2em + calc(0.8em * #{$i}));
  }
  /**
   * The label on options is handled similar to <optgroup label="..."> 
   * (tested with chrome 87 and 95)

   * This styles a <option> similar to an option group
   * but it requires the attribute data-deppth and disabled
   */
  select[size] option.depth-#{$i}[label]:disabled, 
  select[multiple] option.depth-#{$i}[label]:disabled {
    font-weight: bold;
    color: initial;
  }
}

Example Fiddle

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionEd JamesView Question on Stackoverflow
Solution 1 - HtmlRaphael SchweikertView Answer on Stackoverflow
Solution 2 - HtmlAdamView Answer on Stackoverflow
Solution 3 - HtmlEd JamesView Answer on Stackoverflow
Solution 4 - HtmlBroken ArrowView Answer on Stackoverflow
Solution 5 - HtmlTrilceACView Answer on Stackoverflow
Solution 6 - HtmlMatthew GrovesView Answer on Stackoverflow
Solution 7 - HtmlJames BillinghamView Answer on Stackoverflow
Solution 8 - HtmlMatija NalisView Answer on Stackoverflow
Solution 9 - HtmlNickolayView Answer on Stackoverflow
Solution 10 - HtmlDanny LinView Answer on Stackoverflow
Solution 11 - HtmlAlexSp3View Answer on Stackoverflow
Solution 12 - HtmlChristopherView Answer on Stackoverflow