A tabbed form that silently refused to submit — required fields hidden behind another tab

2026年8月28日2 次浏览来源:Dev.to阅读原文

Background The site edit modal kept accumulating fields — site name, category, SSH connection details, WordPress install location — until editing anything meant scrolling up and down a single long form to find the right field.

To clean this up, we split it into three tabs: "Registration info," "SSH," and "WordPress info." That change broke form submission itself, in a way that was hard to spot at first.

What tabbing broke The tab implementation itself is straightforward.

Each tab's fields live in a , and CSS toggles which one is visible.

An inactive tab is hidden with .

Nothing unusual so far, and visually it worked fine.

The problem showed up when a field sat in a tab that was not currently active, and the user left it empty while saving from a different tab.

Clicking the save button did nothing.

No error message appeared.

The form just looked stuck.

Root cause: a browser cannot report an error on a field it cannot show HTML5 form validation works by having the browser automatically block the event whenever a constrained field (like ) fails, then focusing that field and showing its standard validation bubble (equivalent to calling ).

Note: is a method from the HTML5 Constraint Validation API.

It checks whether a form element's value satisfies its constraints (required, pattern, etc.) and, if not, displays the browser's standard error bubble.

But when the failing field sits inside a tab hidden with , the browser has nowhere to anchor that error bubble.

It still faithfully blocks the submit — but it cannot visualize the error, so it simply stops without any visible feedback.

From the user's side, this looks exactly like a button that does not respond.

Before tabbing, every field lived on the same screen, so this never surfaced.

Introducing tabs — a UI pattern that deliberately limits what's visible at once — broke an implicit assumption the browser's built-in validation depends on: that an invalid field is always visible.

The fix: novalidate plus JS-driven validation The fix was to stop relying on the browser's automatic blocking.

Adding to the form disables that automatic block and guarantees the event always fires, letting JavaScript take full control of validation.

Two things matter here. does not disable the constraints themselves.

Attributes like and remain in effect, and both and the pseudo-class still work as expected.

What disables is only the browser's automatic "block submit and show the error" behavior.

The code finds the first element and forces a switch to its tab before calling .

By making the field visible first, the browser can render its error bubble without issue.

Since a tab switch may not have finished painting yet, the code waits one frame via before calling and .

A concrete case: making the SSH profile conditionally required This mechanism paid off directly with the SSH profile requirement.

The site edit modal has an "Update via browser only (no SSH)" checkbox; leaving it unchecked makes selecting an SSH profile mandatory.

We wanted to prevent saving a site that satisfies neither option — no profile selected, and the checkbox left unchecked.

The attribute is toggled dynamically based on the checkbox state, and the rest is handled by the cross-tab validation logic in described above.

Trying to save without a profile selected automatically opens the "SSH" tab and shows the error bubble right on the profile dropdown.

The user never has to hunt for why saving isn't working.

A side benefit: accessibility via ARIA Alongside the tab split, we added / / and / / .

The tab-switching JavaScript keeps in sync as well.

It is not just a visual toggle — screen reader users also get told which tab is currently selected.

Wrap-up Splitting a form into tabs feels like an intuitive cleanup, but it can quietly break an assumption the browser's built-in validation depends on: that an invalid field is always visible on screen.

Disabling the automatic block with , keeping the constraint checks alive through / , and making the fai

分享