FORMS

Custom Select Dropdown

A fully custom dropdown with a clean dark design, smooth opening animation, and selectable options without relying on the browser’s default select styling.

HTML
<div class="custom-dropdown" data-dropdown>

  <label>Choose Component</label>

  <button
    type="button"
    class="dropdown-trigger"
    data-dropdown-trigger
    aria-expanded="false"
  >

    <span data-dropdown-value>
      Select a category
    </span>

    <svg
      class="dropdown-arrow"
      viewBox="0 0 24 24"
      aria-hidden="true"
    >
      <path d="M6 9l6 6 6-6"></path>
    </svg>

  </button>

  <div class="dropdown-menu" data-dropdown-menu>

    <button type="button" data-dropdown-option="Buttons">
      Buttons
    </button>

    <button type="button" data-dropdown-option="Cards">
      Cards
    </button>

    <button type="button" data-dropdown-option="Forms">
      Forms
    </button>

    <button type="button" data-dropdown-option="Navigation">
      Navigation
    </button>

  </div>

</div>
CSS
.custom-dropdown{
  position:relative;
  width:100%;
  max-width:420px;
}

.custom-dropdown > label{
  display:block;
  margin-bottom:9px;
  color:#aab4c8;
  font-size:13px;
  font-weight:600;
}

.dropdown-trigger{
  width:100%;
  min-height:52px;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:16px;
  padding:0 16px;
  border:1px solid #263750;
  border-radius:10px;
  background:#0b1423;
  color:#aab4c8;
  font-size:16px;
  text-align:left;
  cursor:pointer;
  outline:none;
  transition:.25s ease;
}

.dropdown-trigger:hover{
  border-color:#38506f;
}

.dropdown-trigger:focus,
.custom-dropdown.open .dropdown-trigger{
  border-color:#22d3ee;
  box-shadow:0 0 0 3px rgba(34,211,238,.10);
}

.dropdown-trigger.has-value{
  color:#ffffff;
}

.dropdown-arrow{
  flex:0 0 auto;
  width:18px;
  height:18px;
  fill:none;
  stroke:#22d3ee;
  stroke-width:2;
  stroke-linecap:round;
  stroke-linejoin:round;
  transition:transform .25s ease;
}

.custom-dropdown.open .dropdown-arrow{
  transform:rotate(180deg);
}

.dropdown-menu{
  position:absolute;
  z-index:20;
  top:calc(100% + 8px);
  left:0;
  right:0;
  padding:6px;
  border:1px solid #263750;
  border-radius:10px;
  background:#0b1423;
  box-shadow:0 18px 45px rgba(0,0,0,.35);
  opacity:0;
  visibility:hidden;
  transform:translateY(-8px);
  transition:
    opacity .2s ease,
    transform .2s ease,
    visibility .2s ease;
}

.custom-dropdown.open .dropdown-menu{
  opacity:1;
  visibility:visible;
  transform:translateY(0);
}

.dropdown-menu button{
  width:100%;
  display:block;
  padding:11px 12px;
  border:0;
  border-radius:7px;
  background:transparent;
  color:#c7d2e5;
  font-size:15px;
  text-align:left;
  cursor:pointer;
  transition:.2s ease;
}

.dropdown-menu button:hover{
  background:#101c2e;
  color:#ffffff;
}

.dropdown-menu button.selected{
  background:rgba(34,211,238,.10);
  color:#22d3ee;
}
JavaScript
document.addEventListener('click', function(e) {

  const trigger = e.target.closest('[data-dropdown-trigger]');

  if (trigger) {

    const dropdown = trigger.closest('[data-dropdown]');
    const isOpen = dropdown.classList.contains('open');

    document.querySelectorAll('[data-dropdown].open')
      .forEach(function(item) {

        item.classList.remove('open');

        const itemTrigger =
          item.querySelector('[data-dropdown-trigger]');

        if (itemTrigger) {
          itemTrigger.setAttribute('aria-expanded', 'false');
        }

      });

    if (!isOpen) {
      dropdown.classList.add('open');
      trigger.setAttribute('aria-expanded', 'true');
    }

    return;
  }

  const option = e.target.closest('[data-dropdown-option]');

  if (option) {

    const dropdown = option.closest('[data-dropdown]');
    const value = dropdown.querySelector('[data-dropdown-value]');
    const dropdownTrigger =
      dropdown.querySelector('[data-dropdown-trigger]');

    const selectedValue =
      option.getAttribute('data-dropdown-option');

    value.textContent = selectedValue;

    dropdownTrigger.classList.add('has-value');

    dropdown.querySelectorAll('[data-dropdown-option]')
      .forEach(function(item) {
        item.classList.remove('selected');
      });

    option.classList.add('selected');

    dropdown.classList.remove('open');

    dropdownTrigger.setAttribute(
      'aria-expanded',
      'false'
    );

    return;
  }

  document.querySelectorAll('[data-dropdown].open')
    .forEach(function(dropdown) {

      dropdown.classList.remove('open');

      const dropdownTrigger =
        dropdown.querySelector('[data-dropdown-trigger]');

      if (dropdownTrigger) {
        dropdownTrigger.setAttribute(
          'aria-expanded',
          'false'
        );
      }

    });

});

How This Custom Select Dropdown Works

This Custom Select Dropdown creates a menu from regular buttons and JavaScript rather than using a native <select> element. The component has a visible trigger, four selectable options, an arrow icon, and JavaScript that manages opening, closing, and displaying the selected value.

The outer .custom-dropdown container uses position: relative, a width of 100%, and a max-width of 420px. This relative positioning is important because the dropdown menu is absolutely positioned underneath the trigger. A label reading “Choose Component” appears above the control with a 9px bottom margin.

The .dropdown-trigger is a regular type="button" element. It starts with “Select a category” inside the [data-dropdown-value] span and includes an inline SVG arrow. Flexbox places the text and arrow at opposite sides of the 52px minimum-height control.

The trigger has a dark #0b1423 background, a #263750 border, and a 10px border radius. Hovering changes the border to #38506f. When the trigger receives focus or the dropdown has the .open class, the border becomes cyan #22d3ee and a three-pixel cyan-tinted focus ring appears.

The menu is positioned with top: calc(100% + 8px), placing it eight pixels below the control. Its left: 0 and right: 0 values make it follow the width of the dropdown container. A z-index of 20 helps it appear over surrounding content.

In its closed state, .dropdown-menu uses opacity: 0, visibility: hidden, and translateY(-8px). Adding .open to the parent changes these properties to opacity: 1, visibility: visible, and translateY(0). The opacity and transform use .2s ease transitions, creating the opening and closing motion. At the same time, the cyan SVG arrow rotates 180 degrees using a .25s transform transition.

JavaScript manages these CSS states through one click listener attached to document. It first uses e.target.closest('[data-dropdown-trigger]') to determine whether the trigger or something inside it was clicked. If so, it finds the parent dropdown and checks whether .open is already present.

Before opening a dropdown, the script finds any currently open [data-dropdown] elements, removes their .open classes, and changes their aria-expanded attributes to "false". The clicked dropdown can then receive .open and aria-expanded="true". For more detail on the DOM method used to add, remove, and inspect these state classes, see the MDN classList documentation.

Option selection uses [data-dropdown-option] attributes. The available values are Buttons, Cards, Forms, and Navigation. When an option is clicked, JavaScript reads its attribute value, updates the trigger text, and adds .has-value so the selected text becomes white instead of the initial muted color.

The script also removes .selected from every option before applying it to the newly selected button. The selected option receives a translucent cyan background and cyan text. The menu then closes and aria-expanded returns to "false".

Finally, clicking elsewhere on the document closes any open dropdown. This keeps the menu from remaining visible after the user moves to another part of the page.

Customizing the Custom Select Dropdown

The component exposes several practical values through its HTML, CSS, and data-dropdown-option attributes.

  • Change the 420px max-width to control the maximum dropdown size.
  • Edit the trigger’s 52px minimum height, 16px horizontal padding, or 10px border radius to change its proportions.
  • Replace #22d3ee to customize the active border, arrow, focus ring, and selected-option color.
  • Change #0b1423 to use a different background for the trigger and dropdown menu.
  • Adjust the menu’s eight-pixel offset in top: calc(100% + 8px) to move it closer to or farther from the trigger.
  • Modify the .2s menu transitions and .25s arrow transition to change the animation speed.
  • Replace Buttons, Cards, Forms, and Navigation and their data-dropdown-option values with the choices required by your interface.
  • Change the initial “Select a category” text to provide a different selection prompt.

Where to Use This Custom Select Dropdown

This component works well when an interface needs a compact custom menu with a small set of choices.

  • Component category selectors
  • Dashboard filters
  • Sorting controls
  • Account setting menus
  • Product option selectors
  • Form category fields
  • Search filtering interfaces

For forms that need mutually exclusive choices displayed all at once rather than hidden inside a menu, the Custom Radio Buttons component is a useful related alternative. This dropdown manages its own visual selection state, but it does not contain a native <select> or hidden form field for submitting the selected value.

Frequently Asked Questions

Does this Custom Select Dropdown use a native select element?

No. It is built from buttons, spans, data attributes, CSS, and JavaScript. There is no <select> or <option> element in the retrieved code.

How does the dropdown close when clicking outside it?

The document-level click listener checks whether the click belongs to a trigger or option. Otherwise, it finds open dropdowns, removes .open, and resets their aria-expanded attributes.

How is the selected option displayed?

JavaScript reads the clicked button’s data-dropdown-option value and assigns it to the [data-dropdown-value] element using textContent. It also gives the selected option a .selected class.

Can more than one dropdown remain open at once?

No. Before opening a trigger, the script finds currently open dropdowns and closes them. This ensures the interaction keeps only the newly opened menu visible.

This Custom Select Dropdown combines positioned CSS, animated state classes, data attributes, and event delegation to provide a compact category selector with clear selected and open states.

Scroll to Top