Skip to content
Putting technology to work.
Insights to guide decisions and action.

Search articles

How to Build Application Forms with Dynamic Fields Based on Selections — Dynamic and Conditional Branching Form Design in Custom Development

Table of contents · 8 items

"For insurance applications, we need questions to change depending on the chosen plan," "We want to vary job application fields by role," "For quote requests, we want users to add as many item rows as they like" — in custom development, we receive inquiries like these multiple times a year. While it might look as simple as toggling displays based on conditions, building them frequently surfaces subtle pitfalls: hidden field values leaking into submissions, inputs vanishing when navigating back a step, or duplicate submissions creating two identical records.

Dynamic forms are difficult because state management and validation become far more complex than the visual layout suggests. This article explains how to build robust forms with fields that adapt to user selections, multi-step wizards, and dynamically expandable line items using React and Next.js (App Router), covering both implementation insights and custom development execution.

First, establish a single source of truth ― schema-driven validation

The most common point of failure in dynamic forms is scattering validation rules across the UI. If rules like "this input must be a number and is required" or "required only when Plan A is selected" are hardcoded directly into JSX or onChange handlers, consistency collapses as conditions multiply.

The first decision to make is centralizing input structures and validation rules into a single schema. Our standard approach defines schemas with zod and connects them to React Hook Form on the UI. The schema becomes the single definition of what constitutes valid submission data, and TypeScript types can be inferred directly from it.

import { z } from "zod";

export const applicationSchema = z
  .object({
    plan: z.enum(["basic", "pro"]),
    email: z.string().email(),
    // proプランのときだけ必須にしたい項目
    companyName: z.string().optional(),
  })
  .refine(
    (v) => v.plan !== "pro" || !!v.companyName,
    { path: ["companyName"], message: "会社名を入力してください" }
  );

export type Application = z.infer<typeof applicationSchema>;

The crucial technique is encapsulating conditional requirements inside the schema using refine. Because a rule like "company name is required only for Pro" lives in the data definition rather than the UI, you never have to wonder where to place validation logic as new plans are added. While commissioning perspectives on form optimization are covered in What Really Works in Form Optimization (EFO), the foundation of technical robustness rests first on this schema centralization.

Client-side validation is courtesy; server-side validation is defense

What needs emphasis here is that you must never rely exclusively on client-side validation. Validation in the browser is a courtesy to help users notice mistakes prior to submission; it is not a security boundary. Anyone opening developer tools can easily submit data with values entered into hidden fields or bypass validation altogether.

For this reason, we pass incoming data through the exact same zod schema on the server side. Inside App Router Server Actions or Route Handlers, received data is re-validated using safeParse before advancing to database storage.

"use server";
import { applicationSchema } from "@/schemas/application";

export async function submitApplication(formData: unknown) {
  const parsed = applicationSchema.safeParse(formData);
  if (!parsed.success) {
    return { ok: false, errors: parsed.error.flatten() };
  }
  // ここまで来たデータだけを信頼して保存する
  await saveApplication(parsed.data);
  return { ok: true };
}

Maintaining a single schema ensures this dual-layer validation reuses the same definition rather than relying on copy-pasting. It also prevents typical bugs where validation rules drift between client and server, allowing requests to pass on one side only to fail on the other.

Conditional fields and values in hidden inputs

When conditionally toggling fields based on user selection, how you handle hidden field values is easily overlooked. If a user switches from Pro to Basic and a previously entered company name lingers in the background, unwanted values contaminate the submission payload. In some cases, a payload where Basic contains a company name passes server validation, compromising data integrity.

The remedy is establishing a firm policy. As a general rule, our practice is to reset values when fields no longer meet display conditions, rather than just hiding them visually. In React Hook Form, invoking setValue or resetField when observed values change resets fields that have become hidden. Having server-side refine confirm that Basic cannot include a company name preserves data integrity regardless of the order of user actions on screen.

While display logic can be written cleanly with conditional branching, there is one critical caveat regarding accessibility. When adding or removing fields, you must ensure assistive technologies recognize their absence using the hidden attribute or conditional unmounting. Merely hiding elements with CSS still causes screen readers to announce them, leaving users searching for nonexistent fields in confusion. Similar considerations are covered in depth in Building Accessible Forms and Navigation.

Preserving state across multi-step forms

Multi-step wizards are a classic method to reduce information density per screen and curb drop-offs. However, the very act of dividing steps introduces new challenges.

  • Does entered data persist when navigating back to a previous step?
  • Does clicking the browser back button return to the previous form step, or navigate away from the site?
  • If a user reloads mid-process, are they forced to start over from the beginning?

The core decision is whether to maintain the state for all steps within a single form instance and toggle only the visible range for each step. By creating a single form in React Hook Form and rendering visible <section> components based on the active step, values are never lost during navigation between steps. In the "Next" action of each step, validate only the fields belonging to that step with trigger before progressing.

Back navigation and reload resilience depend on requirements. Synchronizing browser history by appending step parameters to the URL (such as ?step=2) allows the back button to function naturally. For lengthy forms where input should survive reloads, in-progress values can be saved to and restored from sessionStorage. However, because this temporarily stores input on the client device, consider limiting saved fields or applying encryption based on data sensitivity. Designing consent and storage transparently and honestly for users aligns closely with Designing Consent and Cancellation Flows That Avoid Dark Patterns.

Line-item forms with dynamically added rows

In forms where users add arbitrary rows — such as quotes or expense reports — managing array state is critical. Mechanisms like useFieldArray in React Hook Form allow appending, removing, and reordering rows safely while preserving entered values in each row.

There are three key points to keep in mind during implementation. First, use a stable key for each row. If you directly pass the array index to key, React can confuse rows when an intermediate row is deleted, leading to an issue where input values are shifted by one row. Second, write validation at the array level as well. With zod, you can define rules such as "at least one row" and "required for each row" like z.array(rowSchema).min(1), and express cross-row rules like the total amount using refine. Third is accessibility. When a row is added with the add button, moving focus to the first input of the new row allows users navigating solely by keyboard to continue smoothly. For error displays, link labels and error messages using aria-describedby so assistive technologies can recognize which field in which row has an issue.

Submission idempotency — preventing duplicate registrations

Finally, something that must always be nailed down in custom development is submission idempotency. When connections are slow, users click the submit button multiple times. If the network is unstable, the same request might be resent. Without any countermeasures, the same application will be registered two or three times.

On the client side, you disable the button during submission and clearly indicate loading with a isSubmitting state. However, this is merely a courtesy, so you place the ultimate safeguard on the server side. Issue a unique token when displaying the form and include it in the request; the server then uses that token to determine whether "this application has already been processed." If it has already been processed, return the same success response without creating a new record. This ensures that even if a user clicks twice, it only results in a single transaction.

Workflow and estimated effort in custom development

In an estimate request form for a certain staffing agency (let's call it Company B) that we recently worked on, we were initially consulted with the expectation that "questions simply change based on plan selection." However, breaking down the requirements revealed three coexisting dynamic elements: there were three types of plans, each with different required fields; the application line items needed dynamic row addition; and they wanted a three-step wizard because users frequently dropped off midway through input.

What drives up the effort in such cases is not the number of screens, but "the combination of validation and state." When conditional branching, multi-step flows, and dynamic arrays intersect, the number of test patterns increases exponentially. At the estimate stage, we carve out schema definition, conditional branching, state persistence, server validation, and idempotency as independent work items, aligning with the client on how far to build out each item this time. Simply deciding upfront "whether the browser back button needs to be supported" or "whether saving progress is necessary" drastically changes the required effort. If you are unsure at the technology selection stage about "whether a full framework is even needed for this form in the first place," choosing between Next.js and Astro also serves as a helpful criterion.

Dynamic forms might look like they are working, but they silently break behind the scenes through hidden fields, back navigation, and duplicate submissions. If you are considering an application form where fields change based on selections, step-by-step splitting to reduce drop-offs, or dynamic addition of line items—and want to build it robustly or fix issues in an existing form—please consult us via Contact. We will assist you starting from requirement decomposition and effort estimation.

Sources

Share this articleXFacebook
Kakeru Suzuki

Fascinated by the possibilities of technology, has had a deep interest in programming and digital art since student days

Turn this article's theme into your company's next step

Concrete steps forward for your organization.

We organize your desired architecture, legacy systems, and operational requirements to formulate your next steps toward execution.

  • Desired architecture
  • Integration with existing environments
  • Operational requirements
Consult on development & operations initiatives

You can consult with us from the initial conceptual stage. Details from this article will be carried over to the inquiry form.

Receive the latest articles by email