HTML show hide div based on dropdown

One listener on the select, one panel per option, and an id that matches the option value. That is the whole pattern.

The HTML show hide div based on dropdown pattern is one listener. On the change event of the <select>, hide every panel, then show the one whose id matches the selected value.

That is the entire mechanism. The parts people get wrong are the first render, the fields that keep submitting, and what happens when the same page is opened by someone else.

A form with a single select at the top and one panel of fields showing beneath it.
A form with a single select at the top and one panel of fields showing beneath it.

The HTML show hide div based on dropdown markup

Keep the option values short and stable. They are doing double duty as element ids.

<label for="kind">Document type</label>
<select id="kind">
  <option value="invoice">Invoice</option>
  <option value="quote">Quote</option>
  <option value="credit">Credit note</option>
</select>

<div id="invoice" class="panel">Invoice number, due date, payment terms.</div>
<div id="quote" class="panel">Validity period, discount, contact.</div>
<div id="credit" class="panel">Original invoice, reason, amount.</div>

<style> .panel { display: none; } .panel.is-shown { display: block; } </style>

<script>
  var sel = document.getElementById('kind');
  function sync() {
    document.querySelectorAll('.panel').forEach(function (p) {
      p.classList.toggle('is-shown', p.id === sel.value);
    });
  }
  sel.addEventListener('change', sync);
  sync();
</script>

The final sync() call is the line most snippets leave out. Without it the page renders with every panel hidden until the reader touches the dropdown.

Why the first render goes wrong

Browsers restore the previously selected option when a page is reloaded. They do not fire change when they do it.

So on a refresh the select says "Quote" while your script still believes nothing is selected. Calling the sync function once at the end of the script closes that gap.

The same applies when you set a default selected option in the markup. The attribute changes what is shown in the select, not what your script has run.

The select expanded, showing the three options with their values.
The select expanded, showing the three options with their values.

Hidden fields are still submitted

This one costs real money in real forms. A <input> inside a div with display: none is still a form control, and its value is still posted.

If you switch from Invoice to Credit note, the invoice fields are invisible and still travelling. Disable them:

function sync() {
  document.querySelectorAll('.panel').forEach(function (p) {
    var on = p.id === sel.value;
    p.classList.toggle('is-shown', on);
    p.querySelectorAll('input, select, textarea').forEach(function (f) {
      f.disabled = !on;
    });
  });
}

A disabled control is left out of the submission and is skipped by the tab order, which is the behaviour you want in both cases.

Which approach fits which form

Situation Approach Script
Three or four options, one panel each Id matching, as above 5 lines
Options grouped into categories data-panel attribute on each option 6 lines
Two choices only Radio buttons and :checked in CSS None
Panels that must animate Class toggle plus a transition 8 lines
Options loaded from data Build the select, then sync More

Pick the row by how the options are likely to grow. Id matching is the shortest to write and the first to break, because it silently couples two things that look unrelated in the source: an option value and an element id.

The data-panel row costs one extra line and survives someone renaming a value six months later. On a form that anyone else will maintain, take it from the start.

The radio button row is worth reading twice. CSS can react to a checked radio and style a following sibling, so a two-way or three-way choice needs no script at all.

input#yes:checked ~ #yes-panel { display: block; }

That only works because the panel is a later sibling of the input. It cannot be done with <select>, because no CSS selector reads a select's current value.

Grouping options without one panel each

When several options share a panel, stop using ids as the link and put the target on the option.

<option value="gbp" data-panel="currency">Pounds</option>
<option value="eur" data-panel="currency">Euro</option>
<option value="btc" data-panel="crypto">Bitcoin</option>

Then read sel.selectedOptions[0].dataset.panel instead of sel.value. The panel ids no longer have to match the values, which keeps the values free for whatever the server expects.

Dependent dropdowns

A related request: the second dropdown's options depend on the first. Region picks a country, country picks a city.

Keep the full option set in the markup and filter it, rather than rebuilding the select from an array.

<select id="city">
  <option value="ber" data-for="de">Berlin</option>
  <option value="ham" data-for="de">Hamburg</option>
  <option value="lyo" data-for="fr">Lyon</option>
</select>
function filterCities() {
  var c = document.getElementById('country').value;
  var city = document.getElementById('city');
  city.querySelectorAll('option').forEach(function (o) {
    o.hidden = o.dataset.for !== c;
  });
  if (city.selectedOptions[0] && city.selectedOptions[0].hidden) city.value = '';
}

That last line matters. Hiding the currently selected option leaves the select showing a value the reader can no longer see, so clear it.

Support for hidden on an option is good in current browsers but not universal. Where it is ignored, remove the options instead and keep a copy of the full list.

Accessibility and the one attribute to add

The panels appear and disappear without the page reloading, so a screen reader user may not notice the change.

Mark the region as live:

<div id="panels" aria-live="polite">...</div>

Keep the label on the select itself, tied by for and id. A select with placeholder text but no label is announced with no name at all. Labelling controls covers the cases where a visible label is not wanted.

Testing it the way a reader sees it

The second option selected, with its own panel replacing the first one.
The second option selected, with its own panel replacing the first one.

Open the file in the HTML file opener, change the dropdown through every option, then reload the page and check the panel still matches.

That reload is the test that catches the missing initial sync, and it is the one almost nobody runs.

Two more passes are worth the minute they take. Narrow the window to phone width and open the select, because a native picker covers most of a small screen and the panel underneath may be off screen when it closes.

Then submit the form with a panel hidden and check what arrived. If fields from the hidden panel are in the submission, the disabling step above is missing.

When it behaves, paste the HTML into a NOS document. The select, the listener and the panels all render at the document's own address, so the reader gets the working form rather than a file they have to download.

Editing later does not move the address. You can correct a label by clicking the text, or go back into the online HTML editor for the script. Turning the form into a link covers what to do with what people type into it.

Questions people ask

How do I show a div when a specific option is selected?

Give each panel an id equal to the option value, listen for the change event on the select, hide every panel, then show the one whose id matches the current value. That is four lines and it scales to any number of options.

Why does the wrong panel show when the page first loads?

Because the change event has not fired yet. The browser restores the previously selected option on reload but does not tell your script about it. Run the same function once immediately after binding the listener.

Can I do this without JavaScript?

Not with a select element. CSS has no sibling selector that reacts to a select value. If you can use radio buttons instead, the checked pseudo class does it with no script at all.

Does the hidden panel still submit its fields?

Yes. A field inside a hidden div is still part of the form and is still sent. Add the disabled attribute to the fields in hidden panels if you want them left out of the submission.

Keep reading