Build a tab-based form with two sections:
Keep all form state in one place above the tabs so switching tabs never loses input; render tabs as accessible tablist/tabpanel; validate per-tab but submit the whole form; show per-tab validity/error indicators; and don't unmount inactive tab content if it would drop state.
A two-section tabbed form is really one form with a segmented UI. The central decision: where does state live, and what happens when you switch tabs?
1. State lives ABOVE the tabs — one source of truth
function TabbedForm() {
const [activeTab, setActiveTab] = useState("personal");
const form = useForm(); // RHF / Formik / single useState object — ONE form
// tabs just render different fields of the SAME form state
}The #1 mistake is per-tab local state — switch tabs, the other tab's inputs unmount, and the data is gone. Hoist all form state above the tabs so it survives tab switches.
2. Don't lose state on tab switch
Two ways to keep inactive-tab data:
- Keep state hoisted (above) and either keep both panels mounted (
hiddenon the inactive one) or unmount freely — the data is safe because it's in the parent, not the panel. - A form library (React Hook Form) stores values centrally, so unmounting a tab's fields doesn't lose them (with
shouldUnregister: false).
3. Accessible tabs
role="tablist",role="tab"(witharia-selected,aria-controls),role="tabpanel"(witharia-labelledby).- Keyboard: Arrow keys move between tabs, the active tab is in the tab order (roving
tabindex), Enter/Space activates. - Or compound components
<Tabs><Tab/><TabPanel/></Tabs>.
4. Validation strategy
- Validate per tab — each section has its own fields and rules.
- Show per-tab status — a checkmark or error dot on each tab so the user knows which section has problems without clicking through. Critical UX: errors on a hidden tab are invisible otherwise.
- Submit validates the whole form — on submit, validate everything; if a hidden tab has errors, switch to it and focus the first error.
- Optionally gate tab switching ("complete this section first") — but usually free navigation + clear indicators is better UX than blocking.
5. Submission
- One submit button (often on the last tab, or persistent) submits the entire form.
- Show overall validity; on failure, route the user to the offending tab.
Edge cases
- Deep-linking to a specific tab (tab in the URL).
- Unsaved-changes warning on navigate-away.
- A field on tab 2 depending on a tab 1 value.
How to answer
"It's one form with a tabbed UI, so all form state lives above the tabs — that's what makes switching tabs lossless. Tabs are an accessible tablist/tabpanel with keyboard support. I validate per-tab but show validity indicators on each tab so hidden errors are visible, and the submit validates the whole form — if a hidden tab has errors I switch to it and focus the first one. A form library like RHF handles the central state cleanly."
Follow-up questions
- •Why must form state live above the tabs?
- •How do you make sure validation errors on a hidden tab aren't missed?
- •Should you block tab switching until a section is valid?
- •What ARIA roles does a tab interface need?
Common mistakes
- •Per-tab local state, so switching tabs loses input.
- •No per-tab validity indicator — errors on hidden tabs are invisible.
- •Submit that fails silently when a hidden tab is invalid.
- •Divs with onClick instead of accessible tab roles + keyboard support.
Performance considerations
- •Keeping both panels mounted (hidden) costs some DOM but makes switching instant and lossless; unmounting is fine if state is hoisted. A form library avoids re-rendering the whole form per keystroke.
Edge cases
- •A field on one tab depending on a value from the other.
- •Deep-linking to a specific tab.
- •Unsaved-changes warning on navigate-away.
- •Submit triggered from a tab that's currently valid while another isn't.
Real-world examples
- •Settings pages, multi-section profile editors, checkout with shipping/payment tabs.