November 13, 2019

Pure CSS Custom Select Dropdown Menus

Note: This is not production-ready code. This was an idea that seems to work. Help is very much needed to make it production-ready.

GitHub Repo: https://github.com/idhavalmehta/custom-select-dropdowns
Live Demo: https://dhaval.xyz/demo/custom-select-dropdowns

HTML:

<div class="select">

  <input class="toggle" type="text" readonly="">
  <div class="arrow"></div>

  <div class="selected">

    <!-- Placeholder -->
    <input id="option-0" class="radio" name="someName" type="radio" checked="">
    <div class="value">
      -- Select --
    </div>

    <!-- Text -->
    <input id="option-1" class="radio" name="someName" value="someValue" type="radio">
    <div class="value">
      Text
    </div>

    <!-- Custom HTML -->
    <input id="option-2" class="radio" name="someName" value="someValue" type="radio">
    <div class="value">
      <div class="icon"></div>
      <div class="text"></div>
    </div>

    <!-- Media -->
    <input id="option-3" class="radio" name="someName" value="someValue" type="radio">
    <div class="value">
      <img src="#" alt="Some Image">
    </div>

  </div>

  <div class="dropdown">

    <!-- Text -->
    <label for="option-1" class="value">
      Text
    </label>

    <!-- Custom HTML -->
    <label for="option-2" class="value">
      <div class="icon"></div>
      <div class="text"></div>
    </label>

    <!-- Media -->
    <label for="option-2" class="value">
      <img src="#" alt="Some Image">
    </label>

  </div>

</div>

CSS:

/*
 * Some basic styles.
 */

.select {
  position: relative;
  font-family: sans-serif;
  vertical-align: middle;
  display: inline-flex;
}

.select .arrow,
.select .toggle {
  cursor: pointer;
}

/*
 * Hide the actual radio buttons.
 */

.select .radio {
  position: absolute;
  opacity: 0 !important;
  z-index: -1;
}

/*
 * Styles for selected value.
 */

.select .selected {
  width: 100%;
  height: 40px;
  font-family: inherit;
  font-size: 16px;
  line-height: 1.5;
  padding-left: 10px;
  padding-right: 36px;
  background: #ffffff;
  border: 2px solid;
  border-color: #212121;
  border-radius: 2px;
  box-sizing: border-box;
  display: block;
}
.select .selected .value {
  line-height: 36px;
}

/*
 * Hide the value sibling
 * if checkbox is not selected.
 */

.select .selected .radio:not(:checked) + .value {
  display: none;
}

/*
 * Styles for dropdown.
 */

.select .dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  padding-top: 4px;
  padding-bottom: 4px;
  background-color: #ffffff;
  border: 2px solid;
  border-top: none;
  border-radius: 0 0 2px 2px;
  box-sizing: border-box;
  overflow: auto;
  z-index: 10;
  transition: opacity 0s, border-color 0s, max-height 0s;
  transition-delay: 0.125s, 0.125s, 0.125s;
}
.select .dropdown .value {
  line-height: 1.5;
  padding-left: 10px;
  padding-right: 10px;
  cursor: pointer;
  display: block;
}
.select .dropdown .value:hover {
  background: lightblue;
}

/*
 * Styles for the dropdown arrow.
 */

.select .arrow {
  position: absolute;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  z-index: 1;
}
.select .arrow:after {
  content: "";
  position: absolute;
  top: 50%;
  right: 12px;
  width: 0;
  height: 0;
  border-width: 6px;
  border-style: solid;
  border-color: transparent;
}

/*
 * Styles for the input field
 * which act as the toggle.
 */

.select .toggle {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: none;
  outline: none;
  border: none;
}

/*
 * If input does not have focus,
 * hide dropdown + some overrides.
 */

.select .toggle:not(:focus) {
  z-index: 2;
}
.select .toggle:not(:focus) ~ .arrow {
  z-index: 1;
}
.select .toggle:not(:focus) ~ .arrow:after {
  border-top-color: #212121;
  border-bottom-color: transparent;
  margin-top: -3px;
}
.select .toggle:not(:focus) ~ .selected {
  border-radius: 2px;
}
.select .toggle:not(:focus) ~ .dropdown {
  max-height: 0;
  border-color: transparent;
  opacity: 0;
}

/*
 * If input has focus,
 * show dropdown + some overrides.
 */

.select .toggle:focus {
  z-index: 1;
}
.select .toggle:focus ~ .arrow {
  z-index: 2;
}
.select .toggle:focus ~ .arrow:after {
  border-top-color: transparent;
  border-bottom-color: #212121;
  margin-top: -9px;
}
.select .toggle:focus ~ .selected {
  border-radius: 2px 2px 0 0;
}
.select .toggle:focus ~ .dropdown {
  max-height: 180px;
  border-color: #212121;
  opacity: 1;
}

The different elements and their purpose:

  • div.select is a wrapper for all the child elements.
  • div.selected is similar to a select i.e. it shows your current selection.
  • div.dropdown is what opens and closes and contains the list of all available options.
  • div.arrow is the arrow indication present at the right in normal select.
  • input.toggle is what causes the open and close behaviour of the dropdown.

CSS allows us to style the focus, hover and checked states of input elements using pseudo-classes. CSS also allows us to select sibling elements using the various sibling selectors available.

The above HTML elements are all siblings of input.toggle and we style them as follows:

  • Show the dropdown if the input.toggle has focus, hide it otherwise.
  • Rotate the arrow by 180° if input.toggle has focus, rotate it back to 0° otherwise.
  • Show the adjacent sibling div.value of an input.radio in div.selected only if the radio button is checked, else hide it.

By default, at least one input.radio in div.selected needs to have the checked attribute. If no option should be selected by default, create a placeholder option and select it as shown in the example.

The label is an interesting element. It does not always have to wrap the input field. You can bind the label to the corresponding input if the input’s ID is specified using the “for” attribute — Learn More

The input.toggle is made transparent and is positioned over div.selected using z-index and absolute positioning thus covering it entirely. When you click on the supposedly select field, you are actually clicking the input field, thus focusing it which shows the sibling dropdown.

By clicking a particular label, it will select the corresponding input.radio, unselecting the rest and triggering blur on the input.toggle which in turn will close the dropdown. This selection change will hide the previously selected value and show the new one thus appearing as a select tag.

If you click anywhere else on the entire page, input.toggle loses focus i.e. a blur is triggered. That will close the dropdown. And as the selected option has not changed yet, no update occurs in the selected value.

Both, the selected and dropdown values can have any kind of content. Some examples are shown in the HTML snippet above. Style it according to your needs. No styles for the examples have been defined in the CSS.

This has been tested on Google Chrome (Mac, Android), Mozilla Firefox (Mac), Opera (Mac) and Safari (Mac).

GitHub Repo: https://github.com/idhavalmehta/custom-select-dropdowns
Live Demo: https://dhaval.xyz/demo/custom-select-dropdowns

If you have any questions, feel free to get in touch with me via email or social media. Raise issues on GitHub so that I can fix them.

Categories: Web Development