jQuery / How to create dependent select controls
Dependent selects change a second select’s options according to the first selection. Listen for the parent’s change event, clear the child list, and populate the matching data. Reset the old child value as well, so an item from the previous group cannot remain selected.
Define data and control roles
This example stores Group A and B with their item arrays locally. There is no server request or additional plugin. Parent value a selects its array. Separate the visible label from the submitted value so changing wording does not change a stable identifier.
Each control has a matching label and unique id; name identifies submitted data. The select element uses required to reject an empty value, and the child is disabled when no items exist. Disabled controls are omitted from submitted data, so missing values and invalid combinations still need server checks.
The blank option is an instruction rather than an actual item. Give real items nonempty identifiers to distinguish a deliberate selection from the prompt.
Complete dependent list example
Save UTF-8 as dependent-select-demo.html and open it. Internet loads full jQuery 4.0.0; no server or build is needed. Windows Edge tests with 4.0.0 and 3.7.1 covered keyboard selection, reset, empty/unknown groups, pair inspection, and repeated switching.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dependent select controls</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
body { font: 16px/1.6 sans-serif; max-width: 720px; margin: 20px; }
label { display: block; margin-top: 12px; }
select, button { font: inherit; max-width: 100%; margin: 6px 0; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<form id="chooser">
<label for="group">Group</label>
<select id="group" name="group" required>
<option value="">Choose a group</option>
<option value="a">Group A</option>
<option value="b">Group B</option>
<option value="empty">Group without items</option>
</select>
<label for="item">Item</label>
<select id="item" name="item" required disabled><option value="">Choose a group first</option></select>
<button type="submit">Inspect selection</button>
<button type="button" id="restore">Reset</button>
</form>
<pre id="result" aria-live="polite"></pre>
<p id="submission" role="status">Nothing inspected.</p>
<script>
jQuery(function ($) {
const choices = new Map([
["a", [{value: "a1", label: "A-1"}, {value: "a2", label: "A < B & C"}]],
["b", [{value: "b1", label: "B-1"}, {value: "b2", label: "B-2"}]],
["empty", []]
]);
const $group = $("#group"), $item = $("#item"), form = document.querySelector("#chooser");
let changes = 0;
function render() {
$("#result").text("group=" + ($group.val() || "(none)") + "\nitem=" + ($item.val() || "(none)") +
"\noptions=" + Math.max(0, $item[0].options.length - 1) + "; disabled=" + $item.prop("disabled") + "\nchanges=" + changes);
}
function rebuildItem() {
const items = choices.get($group.val()) || [];
const prompt = items.length ? "Choose an item" : ($group.val() ? "No available items" : "Choose a group first");
$item.empty().append(new Option(prompt, ""));
for (const item of items) $item.append(new Option(item.label, item.value));
$item.val("").prop("disabled", items.length === 0);
$("#submission").text("Nothing inspected."); render();
}
$group.on("change", function () { changes++; rebuildItem(); });
$item.on("change", function () { changes++; $("#submission").text("Nothing inspected."); render(); });
$(form).on("submit", function (event) {
event.preventDefault();
const items = choices.get($group.val()) || [];
if (!items.some(item => item.value === $item.val())) {
$("#submission").text("Choose a group with an available item."); return;
}
$("#submission").text("Valid pair: " + JSON.stringify(Object.fromEntries(new FormData(form))));
});
$("#restore").on("click", function () { form.reset(); changes = 0; rebuildItem(); });
rebuildItem();
});
</script>
</body>
</html>
Initially neither value is selected, there are no real options beyond the prompt, and the child is disabled.
group=(none)
item=(none)
options=0; disabled=true
changes=0
- Select Group A. Its child shows Choose an item, A-1, and A < B & C: two real items.
- Choose A-1 and Inspect selection. The result is Valid pair: {"group":"a","item":"a1"}. Nothing is sent externally.
- Switch to Group B. B-1 and B-2 replace the options and the child returns to the blank prompt; a1 must not survive.
- Inspect without selecting a child. Native required validation blocks the submit event; wording varies by browser and language.
- Group without items disables the child and displays No available items. Inspection requests a valid available pair.
- Reset restores the initial state. Use arrow keys to check the same selection path.
changes counts this example’s change-handler executions, not option count, clicks, or people. Initial rendering and reset set it to zero. A scripted change event also increments it.
Rebuilding and change events
.on("change", ...) attaches to the two existing controls. The parent value looks up a Map array; an absent key becomes an empty array. Clear the child, insert a prompt and new items, then use .val("") to remove the old choice. Repeated A/B switches therefore do not accumulate options.
new Option(label, value) treats labels as text rather than concatenated HTML. A < B & C stays literal. Reading the selection returns its identifier such as a1 or a2, not necessarily its visible wording.
Setting .val() does not dispatch change automatically. To make another feature choose Group B, run this after initialization so the child is rebuilt through the same event path.
$("#group").val("b").trigger("change");
rebuildItem is private inside the ready callback, not a global function. Use events or design an appropriate public API for other code. If the actual select element is replaced, its direct handlers must also be reconsidered; delegation from a stable parent may help.
Reset calls form.reset() and then recomputes the child list. Native reset alone does not reconstruct options created by JavaScript. The button id is restore, avoiding a reset-named control that could mask the form’s reset method.
Validate pairs and extend carefully
The submit handler prevents normal transmission and checks membership in the current group array. required handles empty enabled selections, but a disabled child in an empty group is excluded from native validation. A valid HTML form and a valid business-level pair are different conditions.
FormData uses named controls and their current values; the disabled child is omitted. This is a browser demonstration. A real server must independently verify the allowed parent/child relationship using its own data.
For three levels, changing the first must clear both second and third lists and values. Populate the third only after a valid second choice. Restoring saved values requires parent data first, then parent selection and child generation, then a valid child value.
Remote loading needs additional handling: clear stale values while waiting, distinguish loading/failure/empty results, and cancel or identify requests so a slow A response cannot overwrite newer B data. No asynchronous request or race handling is implemented here.
If unchanged, check actual IDs and values, load order, and Console errors. If stale values remain, inspect the order of option generation and .val(). Review labels, native mobile controls, and long wording; do not force focus to the next field.




