FullStack Course LogoFullStack Course
Module: Machine Coding
Machine Coding·240·11 MIN READ

240: Forms, Validation, Accessibility, and Interaction States

TOPICS COVERED: Forms, Validation, Accessibility, and Interaction States

Learning outcomes

By the end of this lesson, you can:

  • explain and apply semantic controls in a realistic implementation;
  • explain and apply validation layers in a realistic implementation;
  • explain and apply error announcement in a realistic implementation;
  • explain and apply pending submission in a realistic implementation;
  • explain and apply keyboard and focus in a realistic implementation.

Prerequisites and retrieval

This lesson assumes the 01-06 foundation and the earlier lessons in this module. Before you read, retrieve one concrete example from a previous project in which one of these concerns appeared. Perhaps a form used native inputs, a server rejected a value the client accepted, or a dialog left focus in the wrong place. The point is not to memorize a list of terms. It is to make a defensible decision in a timed machine-coding exercise and still produce code that is readable, accessible, testable, and easy to extend under interview pressure.

Terminology

  • Semantic controls: Prefer native form elements and their labels before recreating controls with div elements. Native elements carry behavior that custom markup does not automatically provide.
  • Validation layers: Client-side validation can provide immediate feedback, but it never replaces validation on the server. Each layer has a different responsibility and trust boundary.
  • Error announcement: Associate an error with the control it describes through accessible descriptions, and move or announce focus appropriately when submission fails. An error that is visible but not exposed to assistive technology is not fully communicated.
  • Pending submission: Prevent accidental duplicate effects while keeping the interface useful. Pending state is a correctness concern, not merely a loading animation.
  • Keyboard and focus: Dialogs, menus, comboboxes, and other custom controls need explicit rules for focus entry, keyboard movement, Escape behavior, and focus return.
  • Progressive enhancement: When the platform or framework permits it, preserve meaningful form semantics even if JavaScript is delayed or a custom enhancement fails.

Mental model

Treat Forms, Validation, Accessibility, and Interaction States as a design problem with observable inputs, outputs, invariants, and failure modes. A form is not correct merely because its happy path submits successfully. Labels, focus placement, keyboard behavior, validation timing, error association, and disabled or pending states all belong to the contract.

The useful question is: what must remain true while the user interacts with the form and while the system is processing the submission? A strong implementation makes its assumptions visible, narrows uncertainty at system boundaries, and leaves enough evidence - tests, types, constraints, metrics, or diagrams - to show why the design is safe.

A practical sequence for an interview or a production change is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement straight to a library call. First state the invariant the interface and server must preserve. Then choose the mechanism that enforces it. For example, if duplicate submission would create two accounts, disabling the button may improve the UI, but the server still needs a way to make retries safe.

Deep dive

1. Semantic controls

When a control is built from a div, the developer must recreate its keyboard behavior, focus behavior, state exposure, and often its form integration. A native button, input, select, or textarea already participates in those browser behaviors. That is why native form elements and labels should be the default starting point rather than a shortcut to replace later.

Decision rule: Use semantic controls deliberately when they make the contract or invariant easier to prove. If a custom control only reduces typing while hiding an assumption about keyboard or submission behavior, choose the more explicit design instead. Customization can be justified, but it also creates behavior that you now have to implement and test.

2. Validation layers

Users benefit from immediate client-side feedback, but the client is not a trusted authority. A browser can be scripted, a request can be replayed, and a stale client may submit a shape that the current UI would no longer produce. Validate again at the server boundary.

Keep raw input strings separate from parsed domain values. An empty string, a malformed number, and a valid number are different states, even if a convenient coercion makes them look similar. Preserve both field-level errors and form-level errors so the UI can identify the exact correction when possible without losing broader failures such as a rejected account or an unavailable service.

Decision rule: Use validation layers deliberately when they make the contract or invariant easier to prove. If a validation helper hides which checks are trusted client hints and which checks protect the server, prefer the more explicit design.

3. Error announcement

An invalid field should not be communicated by color alone. Associate its message with the control through an accessible description, expose the invalid state, and decide where focus should go after a failed submission. A form-level summary can help users understand that errors exist; focusing the first invalid control can put them directly at a place where they can recover. The chosen behavior must be consistent and must not unexpectedly steal focus during ordinary typing.

Decision rule: Use error announcement deliberately when it makes the contract or invariant easier to prove. If it only adds visual styling while leaving keyboard and assistive-technology users without the same information, it is not a complete error treatment.

4. Pending submission

A pending submission is a period in which the user has initiated an effect but the result is not known. The interface should make that state clear and prevent an accidental second submission when a duplicate could create a second side effect. Disabling the submit control may be appropriate. Disabling every field is not automatically correct: users may need to inspect or copy values while the request is pending.

The state also needs a defined failure path. If the request fails, restore useful controls and present an actionable error. If it succeeds, move to a clear success state rather than leaving the user looking at an indefinitely disabled form.

Decision rule: Use pending submission deliberately when it makes the contract or invariant easier to prove. Do not use a broad disabled state merely because it is easy to implement if it prevents useful inspection or recovery.

5. Keyboard and focus

Dialogs, menus, comboboxes, and other custom controls need a focus model before they need key handlers. Define where focus enters, how the user moves through the control, what Escape does, and where focus returns when the interaction ends. Established accessible patterns are safer than improvising behavior one key at a time.

This is where people usually get confused: a visible control is not necessarily a usable control. A mouse click can make a custom menu appear to work while Tab, arrow keys, or Escape leave keyboard users trapped or disoriented. Test the interaction from the keyboard, including reopening it after an error and returning to the triggering element.

Decision rule: Use keyboard and focus deliberately when they make the contract or invariant easier to prove. If an approach depends on a collection of ad hoc key handlers whose entry, movement, escape, and return behavior cannot be stated clearly, prefer an established pattern or a native element.

6. Progressive enhancement

When the platform and framework allow it, keep form semantics meaningful even if JavaScript is delayed or a custom enhancement fails. A real form, labeled controls, and a submit action give the browser a coherent baseline. JavaScript can then improve validation feedback, pending state, and error placement without making the entire interaction depend on a successful enhancement.

Progressive enhancement does not mean pretending every feature works without JavaScript. It means deciding which behavior is fundamental and preserving that behavior at the platform boundary whenever practical.

Decision rule: Use progressive enhancement deliberately when it makes the contract or invariant easier to prove. If an enhancement replaces the platform behavior without preserving an equivalent failure and submission path, the design has hidden a dependency rather than improved the form.

Worked example

Consider a timed machine-coding exercise that must remain readable, accessible, testable, and easy to extend under interview pressure. Start by writing the requirement in one sentence. Then list the input and output contracts and identify which concept owns each failure mode.

The important separation is between boundaries and responsibilities: parsing or validation of untrusted input belongs at the boundary; domain rules belong in the domain or service layer; persistence rules belong in the database or repository; and presentation rules belong in the client. Mixing these concerns can make a happy-path demo look shorter, but it makes edge cases much harder to reason about.

ts
export async function handleRequest(input: unknown) {
  const command = parseCommand(input);
  const result = await service.execute(command);
  return toHttpResponse(result);
}

This example is intentionally small. input is unknown at the boundary, so parseCommand must establish a usable command before the service receives it. The service should not need to understand browser markup, and the response mapper should not be the place where domain validity is discovered.

Walk the design through at least four cases:

  • the normal path;
  • an empty or missing value;
  • a duplicate, retry, or concurrent path where that situation is relevant;
  • a dependency failure.

For each case, state which layer detects the problem and what the caller observes. That is the level of explanation expected in a senior code review or technical interview. It also gives you a direct test plan instead of leaving correctness to the happy path.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failures, stale clients, concurrent requests, malformed data, schema changes, and high-cardinality workloads. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.

When an external dependency is involved, define a timeout and cancellation strategy. When persistence is involved, define transaction and consistency expectations. When the user can see the state, define loading, empty, error, stale, and success states rather than treating them as afterthoughts. When security is involved, assume that the client can be modified and that network input is untrusted. Client validation improves the experience; it does not provide authorization, persistence, or server-side integrity guarantees.

Guided lab

Build an accessible signup or profile form with synchronous field checks, simulated asynchronous server errors, a pending state, error-summary and focus behavior, and completion using only the keyboard.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements.
  2. List the input, output, and error contracts before implementation.
  3. Implement the smallest correct vertical slice.
  4. Add at least one invalid-input test and one edge-case test.
  5. Instrument or inspect the behavior instead of guessing.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it.
  8. Record a short “what would break at 10× scale?” note.

The lab should let you verify more than whether a request eventually succeeds. Check the accessible relationship between each field and its error, the focus result after a failed submit, the behavior during the pending request, and whether a keyboard-only user can complete and recover from the form.

Edge cases and failure modes

For each concept, test more than the normal path. At minimum, cover:

  • Semantic controls: absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Validation layers: absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Error announcement: absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Pending submission: absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.
  • Keyboard and focus: absence, malformed input, duplicates, ordering or concurrency where applicable, and behavior at the smallest and largest credible sizes.

Not every case applies in exactly the same way to every control. The discipline is to ask what “missing,” “malformed,” “duplicate,” “out of order,” and “smallest or largest credible size” mean for that particular contract, rather than mechanically checking boxes.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values.
  • Testing only the happy path and therefore discovering contracts only after integration.
  • Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
  • Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case first. Inspect the actual value or execution plan, trace the boundary where the invariant first becomes false, and fix the layer that owns the problem instead of adding a downstream patch. In a form, that may mean inspecting the DOM and accessible relationships; for a request, the Network panel and server logs; for persistence, the query and transaction behavior. The observation should tell you what to check next, not merely confirm that something failed.

Interview questions

  1. What problem do semantic controls solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem do validation layers solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem does error announcement solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does pending submission solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem do keyboard and focus solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain Forms, Validation, Accessibility, and Interaction States to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or stricter reliability.

References

Reader page: /machine-coding/lesson/240/forms-validation-accessibility-and-interaction-states