Conditional Field Validation Based on Selection

Make a field required and validated only when another input — usually a <select> — holds a specific value, and skip its constraints entirely whenever it is hidden, so the user is never blocked by a rule for a field they cannot even see.

Conditional forms are everywhere: “Other (please specify)”, a shipping address that appears only when “ship to a different address” is checked, a company-name field that matters only for “Employed”. The failure mode is always the same — a constraint left active on a hidden field silently blocks submission with an error the user can never reach. This recipe toggles both visibility and the constraint together, driven by a single source of truth, so the two can never drift apart. It is the field-level companion to the broader progressive disclosure techniques that govern when interface elements appear at all.

When to Use This Recipe

Use conditional validation when a field’s relevance depends on another input’s value:

  • A “specify other” text field that only matters when a select is set to other.
  • A field group revealed by a radio choice or checkbox (employment status, account type, delivery method).
  • Any input that is sometimes required and sometimes irrelevant within the same form.

The governing rule: a hidden field must contribute nothing to validity. If the user cannot see or reach it, its constraints must be inert.

Selection-driven conditional validation A select element drives a dependent field. When the select equals the trigger value, the field becomes visible and required. When it equals any other value, the field is hidden, its value cleared, and its required and pattern constraints removed. select changes value = ? = trigger ≠ trigger show + required validate normally hide + clear remove constraints always valid never blocks submit
The select drives the dependent field: matching the trigger shows and requires it; any other value hides it, clears its value, and strips its constraints so it can never block submission.

Step 1 — Markup with a Trigger and Dependent Field

<form id="profile" novalidate>
  <label for="role">Role</label>
  <select id="role" name="role" required>
    <option value="">Select a role…</option>
    <option value="employee">Employee</option>
    <option value="contractor">Contractor</option>
    <option value="other">Other</option>
  </select>

  <!-- Shown and required only when role === "other" -->
  <div id="other-row" hidden>
    <label for="other-role">Please specify</label>
    <input id="other-role" name="otherRole" type="text" aria-describedby="other-role-error" />
    <p id="other-role-error" class="field-error" role="status" aria-live="polite" hidden></p>
  </div>

  <button type="submit">Save</button>
</form>

The dependent row starts hidden, and the input carries no required or pattern in static markup — constraints are applied only when the field becomes relevant.

Step 2 — Toggle Visibility and Constraints Together

Keep a single function that owns both the DOM visibility and the constraint state, driven by the trigger’s current value. Calling it on change guarantees they never diverge.

const form = document.querySelector<HTMLFormElement>("#profile")!;
const role = form.elements.namedItem("role") as HTMLSelectElement;
const otherRow = document.getElementById("other-row")!;
const otherInput = form.elements.namedItem("otherRole") as HTMLInputElement;

function syncOtherField(): void {
  const active = role.value === "other";

  otherRow.hidden = !active;

  if (active) {
    otherInput.required = true;
    otherInput.pattern = ".{2,}"; // at least 2 characters when relevant
  } else {
    // Hidden → contribute nothing to validity.
    otherInput.required = false;
    otherInput.removeAttribute("pattern");
    otherInput.value = "";                 // clear stale input
    setFieldError(otherInput, null);       // clear any leftover message
  }
}

role.addEventListener("change", syncOtherField);
syncOtherField(); // run once so initial state is correct (e.g. on back-navigation)

Disabling the control is an alternative to removing constraints — a disabled field is excluded from validation and from the submitted FormData. Use disabled when you also want the value omitted from the payload; use constraint removal plus hidden when you want to keep the row in the layout flow but inert.

Step 3 — Validate Only What Is Relevant on Submit

Because hidden fields have no constraints, the standard manual pass simply does the right thing — there is no special-casing in the submit handler.

function setFieldError(input: HTMLInputElement, message: string | null): void {
  const el = document.getElementById(`${input.name}-error`);
  input.setAttribute("aria-invalid", String(message !== null));
  if (el) {
    el.textContent = message ?? "";
    el.hidden = message === null;
  }
}

form.addEventListener("submit", (event) => {
  event.preventDefault();
  let firstInvalid: HTMLInputElement | null = null;

  for (const el of form.querySelectorAll<HTMLInputElement>("input, select")) {
    // checkValidity() returns true for a constraint-free hidden field.
    const ok = el.checkValidity();
    setFieldError(el, ok ? null : el.validationMessage);
    if (!ok && !firstInvalid) firstInvalid = el;
  }

  if (firstInvalid) firstInvalid.focus();
  else void save(new FormData(form));
});

Option Reference

Technique When relevant Excluded from validity? Excluded from FormData?
hidden + remove required/pattern Keep row in markup, inert ✅ (no constraints) ❌ (still submitted, but empty)
disabled the field Omit value entirely
Remove the node from the DOM Field truly never applies
Re-run syncOtherField on load Restore correct state after back-navigation n/a n/a

Verification Steps

Edge Cases & Failure Modes

Stale value submitted from a hidden field. If the user fills the dependent field, then changes the select away from the trigger, the old value can still be sent. Clear value whenever the field becomes irrelevant, as shown.

Constraint left active on a hidden field. This is the classic bug: the field is visually hidden but still required, so form.checkValidity() returns false with an error pointing at an element the user cannot see, and the form silently refuses to submit. Always remove the constraint when hiding.

Forgetting the initial sync. On first load — or when a browser restores form state after back-navigation — the dependent field may already match the trigger. Call syncOtherField() once at startup so constraints and visibility reflect the restored value rather than the static markup.

Frequently Asked Questions

Should I use hidden, disabled, or remove the constraints?

If you want the field's value omitted from the submission entirely, disabled is cleanest — a disabled control is skipped by both constraint validation and FormData. If you want to keep the row visible-in-markup but inert, hide it and remove required/pattern, and clear its value. The one thing you must never do is leave a constraint active on a field the user cannot reach.

Why does my form refuse to submit with no visible error?

Almost always a hidden field that is still required. Constraint validation runs against every control regardless of visibility, so a hidden required field makes the form invalid while its error message has nowhere to display. Removing the constraint whenever you hide the field — and clearing its value — resolves it.

Do I need to re-validate when the dependent field appears?

Not immediately — applying required the moment the field appears, before the user has touched it, would flash a premature error. Add the constraint on reveal but defer the message until the field is touched or the form is submitted, consistent with gating validation behind a touched state. The submit pass then catches it if the user leaves it blank.

← Back to Progressive Disclosure Techniques