A zillion years ago (Windows 98! Internet Explorer 6!) I had an HTML combo box solution on this page that has since become wholly outdated and unusable. Rather than deleting the page, here's a modern implementation created with the help of the Gemini AI. I hope it proves useful.

Here's an example, a combo box of fruits:  

Apple
Banana
Blueberry
Cherry
Cranberry
Durian
Elderberry
Fig
Grape
Grapefruit
Kiwi
Lemon
Lime
Mango
Orange
Papaya
Peach
Pear
Pineapple
Plum
Raspberry
Strawberry
Watermelon

Selected Fruit Value: None

This implementation is a tad more bloated than what I had but I cannot argue with "it works". The one thing I'd tweak is the idea of restricting dropdown contents to match the currently entered text. Then again, if the dropdown contains a lot of options, this can actually prove useful, so I'm leaving it on.

Without further ado, here is the source code:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pure JS Combo Box</title>
  <style>
/* Basic styling for the combo box */
.combobox
{
  position: relative;
  display: inline-block;
  font-family: sans-serif;
}

.combobox-input
{
  padding: 8px 30px 8px 10px; /* Increased right padding for button */
  border: 1px solid #ccc;
  border-radius: 4px;
  width: 200px; /* Adjust as needed */
  box-sizing: border-box; /* Include padding and border in the element's total width and height */
  vertical-align: middle; /* Align input and button nicely */
}

.combobox-input:focus
{
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

/* Style the dropdown button */
.combobox-button
{
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 25px; /* Width of the button */
  background-color: #eee;
  border: 1px solid #ccc;
  border-left: none; /* Remove left border */
  border-radius: 0 4px 4px 0; /* Match input corners */
  cursor: pointer;
  display: flex; /* Use flexbox for centering arrow */
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  color: #333;
}

.combobox-button:hover
{
  background-color: #ddd;
}

/* Simple arrow using text character */
.combobox-button::after
{
  content: '▼'; /* Downward arrow character */
  font-size: 10px;
}

/* Adjust position slightly when input is focused */
.combobox-input:focus + .combobox-button
{
   border-color: #007bff;
   box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
   /* Optional: Remove shadow from the button itself if input has it */
   /* box-shadow: none; */
}

.combobox-options
{
  display: none; /* Hidden by default */
  position: absolute;
  border: 1px solid #ccc;
  border-top: none;
  border-radius: 0 0 4px 4px;
  max-height: 150px; /* Limit height and make scrollable */
  overflow-y: auto;
  background-color: white;
  width: 100%; /* Make options list span the full width of the container */
  /* --- Ensure these are present --- */
  left: 0; /* Align with the left edge of the container */
  box-sizing: border-box; /* Include padding and border in the element's total width */
  z-index: 1000; /* Ensure it appears above other elements */
  margin-top: -1px; /* Overlap slightly with input border */
}

.combobox-options.visible
{
  display: block;
}

.combobox-option
{
  padding: 8px 10px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.combobox-option:hover,
.combobox-option.highlighted
{ /* Style for hover and keyboard navigation highlight */
  background-color: #f0f0f0;
}

.combobox-option.selected
{
  background-color: #007bff;
  color: white;
}

/* Style for options that are filtered out (optional) */
.combobox-option.hidden
{
  display: none;
}
  </style>
</head>
<body>

<h1>Custom Combo Box Example</h1>

<p>Select a Fruit: <span id="fruit-combobox-container">
  <!-- The combo box will be generated here by JavaScript -->
</span></p>

<p>Selected Fruit Value: <span id="selected-fruit-value">None</span></p>

<script>
function createComboBox(container, optionsData, idPrefix, onSelectCallback)
{
  // --- Create Elements ---
  const wrapper = document.createElement('div');
  wrapper.className = 'combobox';
  wrapper.id = `${idPrefix}-wrapper`;

  const input = document.createElement('input');
  input.type = 'text';
  input.className = 'combobox-input';
  input.placeholder = 'Select or type...';
  input.id = `${idPrefix}-input`;
  input.setAttribute('autocomplete', 'off'); // Prevent browser autocomplete

  // --- NEW: Create the dropdown button ---
  const button = document.createElement('button');
  button.type = 'button'; // Important for forms
  button.className = 'combobox-button';
  button.id = `${idPrefix}-button`;
  button.setAttribute('aria-label', 'Toggle options'); // Accessibility
  button.tabIndex = -1; // Prevent button from being tab-focusable itself

  const optionsList = document.createElement('div');
  optionsList.className = 'combobox-options';
  optionsList.id = `${idPrefix}-options`;

  // Store the actual selected value
  let currentSelectedValue = null;
  let currentHighlightedIndex = -1; // For keyboard navigation

  // --- Populate Options ---
  // (This part remains the same as before)
  const optionElements = optionsData.map((option, index) =>
  {
    const optElement = document.createElement('div');
    optElement.className = 'combobox-option';
    optElement.textContent = option.text;
    optElement.dataset.value = option.value; // Store value in data attribute
    optElement.dataset.index = index; // Store original index
    optElement.id = `${idPrefix}-option-${index}`;
     // Click listener for each option
    optElement.addEventListener('mousedown', (e) =>
    { // Use mousedown to prevent blur before click registers
      e.preventDefault(); // Prevent input losing focus
      selectOption(optElement);
    });
    optionsList.appendChild(optElement);
    return optElement; // Keep reference for filtering/navigation
  });

  // --- Append elements in order ---
  wrapper.appendChild(input);
  wrapper.appendChild(button); // Add button next to input
  wrapper.appendChild(optionsList);
  container.appendChild(wrapper);

  // --- Event Listeners ---

  // Show options on input focus or click
  input.addEventListener('focus', showOptions);
  input.addEventListener('click', showOptions); // Handle case where it already has focus

  // Filter options on input typing
  input.addEventListener('input', handleInput);

  // Hide options on clicking outside (Check if click was on button)
  document.addEventListener('click', (e) =>
  {
    // Check if the click is outside the wrapper AND not on the button itself
    if (!wrapper.contains(e.target))
    {
      hideOptions();
    }
  });

  // Keyboard Navigation (remains the same)
  input.addEventListener('keydown', handleKeyDown);

  // --- NEW: Button click listener ---
  button.addEventListener('click', (e) =>
  {
    e.stopPropagation(); // Prevent document click listener from firing
    // Toggle options list visibility
    if (optionsList.classList.contains('visible'))
    {
      hideOptions();
    }
    else
    {
      input.focus(); // Focus input first to ensure keyboard nav works
      showOptions();
    }
  });

  // --- Helper Functions ---

  function showOptions()
  {
    // If input has value, filter before showing
    if(input.value)
    {
      filterOptions();
    }
    else
    {
       // If input is empty, show all options
       optionElements.forEach(opt => opt.classList.remove('hidden'));
    }
    // Only show if there are visible options or input is empty
    const anyVisible = optionElements.some(opt => !opt.classList.contains('hidden'));
    if (anyVisible || !input.value)
    {
      optionsList.classList.add('visible');
    }
    else
    {
      optionsList.classList.remove('visible'); // Hide if filter results in no options
    }
    resetHighlight();
  }

  function hideOptions()
  {
    optionsList.classList.remove('visible');
    resetHighlight();

    // Optional: If text doesn't match a selected value, clear or reset
    const matchingOption = optionElements.find(opt => 
                             opt.textContent.toLowerCase() === input.value.toLowerCase() &&
                             !opt.classList.contains('hidden'));
    if (!matchingOption && currentSelectedValue)
    {
      // If user typed something invalid after selecting, revert to last valid selection
      const selectedOpt = optionElements.find(opt => opt.dataset.value === currentSelectedValue);
      input.value = selectedOpt ? selectedOpt.textContent : '';
    }
    else 
    if (!matchingOption && !currentSelectedValue)
    {
       // Or clear if nothing valid was ever selected
       // input.value = ''; // Decide desired behavior
    }
  }

  function filterOptions()
  {
    const filterText = input.value.toLowerCase();
    let visibleOptionsExist = false;
    optionElements.forEach(optElement =>
    {
      const optionText = optElement.textContent.toLowerCase();
      if (optionText.includes(filterText))
      {
        optElement.classList.remove('hidden');
        visibleOptionsExist = true;
      }
      else
      {
        optElement.classList.add('hidden');
      }
    });
    // If input has text, ensure the dropdown stays visible if there are matches
    if (filterText && visibleOptionsExist)
    {
       optionsList.classList.add('visible');
    }
    else
    if (!visibleOptionsExist)
    {
       optionsList.classList.remove('visible'); // Hide if no matches
    }
  }

  function handleInput()
  {
    // Reset selection when user types
    if (currentSelectedValue)
    {
       const selectedOpt = optionElements.find(opt => opt.dataset.value === currentSelectedValue);
       if(selectedOpt) selectedOpt.classList.remove('selected');
       currentSelectedValue = null;
       updateSelectedDisplay(null);
    }

    filterOptions();
    resetHighlight(); // Reset keyboard highlight

    // Don't automatically *hide* here, filterOptions handles visibility based on results
    // Only ensure it *becomes* visible if typing yields results and it was hidden
    const anyVisible = optionElements.some(opt => !opt.classList.contains('hidden'));
    if (input.value && anyVisible && !optionsList.classList.contains('visible'))
    {
      optionsList.classList.add('visible');
    }
  }

  function selectOption(optionElement)
  {
    if (!optionElement || optionElement.classList.contains('hidden')) return;

    input.value = optionElement.textContent;
    currentSelectedValue = optionElement.dataset.value;

    // Update visual selection state
    optionElements.forEach(opt => opt.classList.remove('selected'));
    optionElement.classList.add('selected');

    hideOptions();
    updateSelectedDisplay(currentSelectedValue); // Call the callback
    // No need to explicitly focus input here, it likely still has focus or lost it naturally
  }

  // handleKeyDown, highlightNextOption, highlightPreviousOption,
  // updateHighlight, resetHighlight, updateSelectedDisplay functions remain the same
  // ... (include the keyboard navigation functions from the previous version here) ...
  function handleKeyDown(e)
  {
    const visibleOptions = optionElements.filter(opt => !opt.classList.contains('hidden'));
    // Don't act if list hidden unless ArrowDown/Up/Enter to open/validate
    const isOptionsVisible = optionsList.classList.contains('visible');

    switch (e.key)
    {
    case 'ArrowDown':
      e.preventDefault(); // Prevent cursor moving in input
      if (!isOptionsVisible)
      {
        showOptions(); // Show all options if closed
      }
      else if (visibleOptions.length > 0)
      {
        highlightNextOption(visibleOptions);
      }
      break;
    case 'ArrowUp':
      e.preventDefault(); // Prevent cursor moving in input
      if (!isOptionsVisible)
      {
        showOptions(); // Show all options if closed
      }
      else
      if (visibleOptions.length > 0)
      {
        highlightPreviousOption(visibleOptions);
      }
      break;
    case 'Enter':
      e.preventDefault(); // Prevent form submission
      if (isOptionsVisible && currentHighlightedIndex !== -1 && visibleOptions.length > 0)
      {
        selectOption(visibleOptions[currentHighlightedIndex]);
      }
      else
      {
        // If options not visible OR no item highlighted, try to match current input text exactly
        const exactMatch = optionsData.find(opt => opt.text.toLowerCase() === input.value.toLowerCase());
        if (exactMatch)
        {
          // Find the corresponding element to pass to selectOption
          const exactMatchElement = optionElements.find(el => el.dataset.value === exactMatch.value);
          if (exactMatchElement)
          {
            selectOption(exactMatchElement);
          }
          else
          {
            hideOptions(); // Hide if somehow element not found
          }
        }
        else
        {
          // Otherwise, just hide the list if it was open
          hideOptions();
        }
      }
      break;
    case 'Escape':
      e.preventDefault(); // Prevent potential browser actions
      if (isOptionsVisible)
      {
        hideOptions();
        // Revert input to last selected value if Escape is pressed
        const selectedOpt = optionElements.find(opt => opt.dataset.value === currentSelectedValue);
        input.value = selectedOpt ? selectedOpt.textContent : '';
      }
      break;
    case 'Tab':
      hideOptions(); // Hide options when tabbing away
      break;
    default:
      // When typing other keys, reset highlight as list content changes
      resetHighlight();
    }
  }

  function highlightNextOption(visibleOptions)
  {
    resetHighlight(false); // Clear previous highlight visually
    currentHighlightedIndex++;
    if (currentHighlightedIndex >= visibleOptions.length)
    {
      currentHighlightedIndex = 0; // Wrap around
    }
    updateHighlight(visibleOptions);
  }

  function highlightPreviousOption(visibleOptions)
  {
    resetHighlight(false); // Clear previous highlight visually
    currentHighlightedIndex--;
    if (currentHighlightedIndex < 0)
    {
      currentHighlightedIndex = visibleOptions.length - 1; // Wrap around
    }
    updateHighlight(visibleOptions);
  }

  function updateHighlight(visibleOptions)
  {
    if (currentHighlightedIndex >= 0 && currentHighlightedIndex < visibleOptions.length)
    {
      const highlightedOption = visibleOptions[currentHighlightedIndex];
      highlightedOption.classList.add('highlighted');
      // Scroll into view if needed
      highlightedOption.scrollIntoView({ block: 'nearest' });
    }
  }

  function resetHighlight(resetIndex = true)
  {
    optionElements.forEach(opt => opt.classList.remove('highlighted'));
    if (resetIndex)
    {
      currentHighlightedIndex = -1;
    }
  }

  function updateSelectedDisplay(value)
  {
    if (onSelectCallback && typeof onSelectCallback === 'function')
    {
      onSelectCallback(value);
    }
  }
} // End of createComboBox function


// --- Data for the Combo Box ---
const fruitOptions =
[
  { value: 'apple', text: 'Apple' },
  { value: 'banana', text: 'Banana' },
  { value: 'blueberry', text: 'Blueberry' },
  { value: 'cherry', text: 'Cherry' },
  { value: 'cranberry', text: 'Cranberry' },
  { value: 'durian', text: 'Durian' },
  { value: 'elderberry', text: 'Elderberry' },
  { value: 'fig', text: 'Fig' },
  { value: 'grape', text: 'Grape' },
  { value: 'grapefruit', text: 'Grapefruit' },
  { value: 'kiwi', text: 'Kiwi' },
  { value: 'lemon', text: 'Lemon' },
  { value: 'lime', text: 'Lime' },
  { value: 'mango', text: 'Mango' },
  { value: 'orange', text: 'Orange' },
  { value: 'papaya', text: 'Papaya' },
  { value: 'peach', text: 'Peach' },
  { value: 'pear', text: 'Pear' },
  { value: 'pineapple', text: 'Pineapple' },
  { value: 'plum', text: 'Plum' },
  { value: 'raspberry', text: 'Raspberry' },
  { value: 'strawberry', text: 'Strawberry' },
  { value: 'watermelon', text: 'Watermelon' }
];

// --- Initialize the Combo Box ---
window.onload = function()
{
  createComboBox(document.getElementById('fruit-combobox-container'), fruitOptions, 'fruit-combo', (selectedValue) =>
  {
    // Callback function to update display when a value is selected
    document.getElementById('selected-fruit-value').textContent = selectedValue || 'None';
    console.log('Selected value:', selectedValue);
  });
}
</script>
</body>
</html>