<div>
  <h1>Custom Dropdown</h1>
  <div class="container">
    <label for="select">Custom Select</label>
    <button
      role="combobox"
      id="select"
      value="Select"
      aria-controls="listbox"
      aria-haspopup="listbox"
      tabindex="0"
      aria-expanded="false">
      Select
    </button>
    <div id="announcement" aria-live="assertive" role="alert"></div>
    <ul role="listbox" id="listbox">
      <li role="option">Mecury</li>
      <li role="option">Eggs</li>
      <li role="option">Venus</li>
      <li role="option">Earth</li>
      <li role="option">Mars</li>
      <li role="option">Jupiter</li>
      <li role="option">Saturn</li>
      <li role="option">Elephant</li>
      <li role="option">Uranus</li>
      <li role="option">Neptune</li>
      <li role="option">Eleplast</li>
      <li role="option">Pluto (I'm a planet)</li>
    </ul>
  </div>
  <div class="container">
      <label for="select2">Default Select</label>
      <select id="select2">
      <option>Mecury</option>
      <option>Venus</option>
      <option>Earth</option>
      <option>Mars</option>
      <option>Jupiter</option>
      <option>Saturn</option>
      <option>Uranus</option>
      <option>Neptune</option>
      <option>Eggsd</option>
      <option>Pluto (I'm a planet)</option>
    </select>
    </div>
    </div>
body {
  display: grid;
  place-content: center;
  height: 100vh;
  margin: 0;
  background: #0a405b;
  color: #fff;
  font-family: "Montserrat", sans-serif;
  font-size: 20px;
}

* {
  margin: 0;
  box-sizing: border-box;
}


.container {
  margin: 1.2rem 0;
  position: relative;
  #announcement {
    opacity: 0;
    pointer-events: none;
  }
  label {
    display: block;
    padding: .7rem .8rem;
    width: 65%;
    margin: 0 auto;
    text-align: left;
    font-size: .75rem;
  }
  select,
  button,
  ul{
    display: block;
    padding: .7rem .8rem;
    width: 60%;
    margin: 0 auto;
    text-align: left;
    background: white;
    border: 0;
    font-size: 1rem;
  }
  button{
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    
    &::before {
      font-family: "Font Awesome 5 Free";
      content: "\f107";
      vertical-align: middle;
      font-weight: 900;
      position: absolute;
      right: .8rem;
    }

    &:focus-visible {
      outline: 0;
      // outline-offset: -3px;
      box-shadow: 0 0 5px 2px rgba(251, 146, 60, 0.7) inset;
    }
  }
  ul {
    color: #3f403b;
    position: absolute;
    left: 0;
    right: 0;
    top: 4.8rem;
    max-height: 10rem;
    overflow-y: auto;
    list-style-type: none;
    padding: 0;
    margin-top: .1rem;
    opacity: 0;
    transform: scale(1,0);
    transform-origin: top left;
    transition: all .3s ease-in;
    pointer-events: none;
    z-index: 2;
    &.active {
      opacity: 1;
      transform: scale(1,1);
      pointer-events: auto;
    }
    li {
      padding: .6rem .5rem;
      border-top: 1px solid #e6e6e6;
      cursor: pointer;
      transition: all .3s ease-in;
      position: relative;
      &::before {
        font-family: "Font Awesome 5 Free";
        content: "\f00c";
        vertical-align: middle;
        font-weight: 900;
        position: absolute;
        right: .8rem;
        opacity: 0;
        transition: opacity .300s ease-out;
      }
      &:hover, &.current {
        background: #e6e6e6;
      }
      &.active {
        // border: 2px solid;
        box-shadow: 0 0 0 2px rgba(251, 146, 60, 0.7);
      }
      &.active::before {
        opacity: 1;
      }
    }
  }
}
View Compiled
const elements = {
  button: document.querySelector('[role="combobox"]'),
  dropdown: document.querySelector('[role="listbox"]'),
  options: document.querySelectorAll('[role="option"]'),
  announcement: document.getElementById('announcement'),
};

let isDropdownOpen = false;
let currentOptionIndex = 0;
let lastTypedChar = '';
let lastMatchingIndex = 0;

const toggleDropdown = () => {
  elements.dropdown.classList.toggle('active');
  isDropdownOpen = !isDropdownOpen;
  elements.button.setAttribute('aria-expanded', isDropdownOpen.toString());

  if (isDropdownOpen) {
    focusCurrentOption();
  } else {
    elements.button.focus();
  }
};

const handleKeyPress = (event) => {
  event.preventDefault();
  const { key } = event;
  const openKeys = ['ArrowDown', 'ArrowUp', 'Enter', ' '];

  if (!isDropdownOpen && openKeys.includes(key)) {
    toggleDropdown();
  } else if (isDropdownOpen) {
    switch (key) {
      case 'Escape':
        toggleDropdown();
        break;
      case 'ArrowDown':
        moveFocusDown();
        break;
      case 'ArrowUp':
        moveFocusUp();
        break;
      case 'Enter':
      case ' ':
        selectCurrentOption();
        break;
      default:
        // Handle alphanumeric key presses for mini-search
        handleAlphanumericKeyPress(key);
        break;
    }
  }
};

const handleDocumentInteraction = (event) => {
  const isClickInsideButton = elements.button.contains(event.target);
  const isClickInsideDropdown = elements.dropdown.contains(event.target);

  if (isClickInsideButton || (!isClickInsideDropdown && isDropdownOpen)) {
    toggleDropdown();
  }

  // Check if the click is on an option
  const clickedOption = event.target.closest('[role="option"]');
  if (clickedOption) {
    selectOptionByElement(clickedOption);
  }
};

const moveFocusDown = () => {
  if (currentOptionIndex < elements.options.length - 1) {
    currentOptionIndex++;
  } else {
    currentOptionIndex = 0;
  }
  focusCurrentOption();
};

const moveFocusUp = () => {
  if (currentOptionIndex > 0) {
    currentOptionIndex--;
  } else {
    currentOptionIndex = elements.options.length - 1;
  }
  focusCurrentOption();
};

const focusCurrentOption = () => {
  const currentOption = elements.options[currentOptionIndex];
    const optionLabel = currentOption.textContent;

  currentOption.classList.add('current');
  currentOption.focus();

  // Scroll the current option into view
  currentOption.scrollIntoView({
    block: 'nearest',
  });

  elements.options.forEach((option, index) => {
    if (option !== currentOption) {
      option.classList.remove('current');
    }
  });
    announceOption(`You're currently focused on ${optionLabel}`); // Announce the selected option within a delayed period
};

const selectCurrentOption = () => {
  const selectedOption = elements.options[currentOptionIndex];
  selectOptionByElement(selectedOption);
};

const selectOptionByElement = (optionElement) => {
  const optionValue = optionElement.textContent;

  elements.button.textContent = optionValue;
  elements.options.forEach(option => {
    option.classList.remove('active');
    option.setAttribute('aria-selected', 'false');
  });

  optionElement.classList.add('active');
  optionElement.setAttribute('aria-selected', 'true');

  toggleDropdown();
  announceOption(optionValue); // Announce the selected option
};

const handleAlphanumericKeyPress = (key) => {
  const typedChar = key.toLowerCase();
  
  if (lastTypedChar !== typedChar) {
    lastMatchingIndex = 0;
  }

  const matchingOptions = Array.from(elements.options).filter((option) =>
    option.textContent.toLowerCase().startsWith(typedChar)
  );

  if (matchingOptions.length) {
    if (lastMatchingIndex === matchingOptions.length) {
      lastMatchingIndex = 0;
    }
    let value = matchingOptions[lastMatchingIndex]
    const index = Array.from(elements.options).indexOf(value);
    currentOptionIndex = index;
    focusCurrentOption();
    lastMatchingIndex += 1;
  }
  lastTypedChar = typedChar;
};

const announceOption = (text) => {
  elements.announcement.textContent = text;
  elements.announcement.setAttribute('aria-live', 'assertive');
  setTimeout(() => {
    elements.announcement.textContent = '';
    elements.announcement.setAttribute('aria-live', 'off');
  }, 1000); // Announce and clear after 1 second (adjust as needed)
};


elements.button.addEventListener('keydown', handleKeyPress);
document.addEventListener('click', handleDocumentInteraction);
Run Pen

External CSS

  1. https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500&amp;display=swap
  2. https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.