Search code examples
javascripthtmlcssdrop-down-menusemantics

Correct semanticly select styling


I want to create a custom-styled select element in my web app. However, I'm concerned about the approach I should use. Styling the default select gives me some options, but I can't fully style the pop-up menu, which is problematic. The other approach I can use to make this custom select is to create it with my own JavaScript; however, it will no longer be a select element semantically, and it won't be recognized in a form, for example. How can I create a select element with my own custom styles but keep it semantically correct?

Maybe I should make it on a label, but how can I then ensure the selecting functions are semantically correct? I can style it and even get it from the original select element and display a custom dropdown made with JavaScript, which will then switch the original select. However, this is not something that the browser will recognize. Or maybe I'm wrong, and it doesn't have any impact, because I think if it's possible to switch the select via JavaScript, I can do that to solve the problem of forms not recognizing the select.


Solution

  • Semantically, <select> element can be replaced with a set of radio buttons -- <input type="radio"> (* for <select multiple> with checkboxes), which obviously would work correctly as form's elements including support of keyboard tab and arrow keys.

    See the snippet below, I'm using <fieldset> containing <input type="radio"> along with corresponding <label> elements.
    This is stylable and expandable with any CSS/HTML. In this case as a dropdown list.

    const toggleSelect = e => {
      const E = e.target.closest('fieldset.select label');
      E && E.closest('fieldset.select').classList.toggle('open');
    }
    
    document.addEventListener('click', toggleSelect);
    
    const logData = e => e.preventDefault(console.log(Object.fromEntries(new FormData(e.target))));
    * {box-sizing:border-box}
    
    .select {
      position: relative;
      display: inline-flex;
      flex-flow: column;
      height: 1.75em;
      overflow: hidden;
      margin:0; padding: 0;
      border: 0;
      
      &:after {
        content: '\25bc';
        position: absolute;
        top:.25em; right:.5em;
        z-index:1;
        pointer-events:none;
      }
      
      &.open {
        height:auto;
        margin-bottom: -100%;
        
        label:first-of-type~label {position:static !important}
      }
      
      input {
        position: absolute; top:.25em;
        z-index: -1;
        opacity: 0;
      }
      
      label {
        padding: .25em 1.5em .25em .5em;
        line-height: 1.25;
        background: #eee;
        
        &:first-of-type {position:static}
        img {width:1.25em; height:1.25em; vertical-align:middle}
      }
      
      label:hover, input:focus+label, input:checked+label {background:#ad7}
      
      label:first-of-type ~ input:checked+label {
        position: absolute; top:0;
        width: 100%;
      }
    }
    <form onsubmit="logData(event)">
      <select name="native-select">
        <option value="1">option 1</option>
        <option value="2"><img src="https://graph.facebook.com/1841494189551870/picture?type=large"></option>
        <option value="3">option 3</option>
        <option value="4">option 4</option>
      </select>
      <hr>
      <fieldset class="select">
        <input name="custom-select" id="option-1" type="radio" value="1" checked>
        <label for="option-1">option 1</label>
        <input name="custom-select" id="option-2" type="radio" value="2">
        <label for="option-2"><img src="https://graph.facebook.com/1841494189551870/picture?type=small"></label>
        <input name="custom-select" id="option-3" type="radio" value="3">
        <label for="option-3">option <i>3</i></label>
        <input name="custom-select" id="option-4" type="radio" value="4">
        <label for="option-4">option 4</label>
      </fieldset>
      <hr>
      <p>Submit to see the resulting data.</p>
      <button>Submit</button> 
    </form>