Forms, Validation & Submission
Turn demo controls into production forms with state, client feedback, server validation, and accessible errors.
On this page 5
What you will learn
- Build a controlled form.
- Validate at both trust boundaries.
- Present errors accessibly.
Use the existing visual states
Form examples show labels, helper text, error text, disabled controls, readonly controls, density, and variants. Keep these visual contracts when connecting real state.
- Every control has a persistent label.
- Required fields are explained.
- Errors identify a resolution.
- Disabled and readonly are used intentionally.
Validate twice
Client validation improves speed and clarity. Server validation is authoritative because browser requests can be modified or bypass the UI.
Submission lifecycle
A reliable form has explicit idle, submitting, success, and failure states.
- Normalize input.
- Run client checks.
- Disable duplicate submission.
- Send the authenticated request.
- Map field errors and form errors.
- Confirm success and update the surrounding view.
Accessible errors
Connect an invalid input to its message using aria-describedby, set aria-invalid, and place a form-level summary where keyboard and screen-reader users encounter it.
tsx
<input
id="email"
name="email"
aria-invalid={Boolean(errors.email)}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email ? <p id="email-error">{errors.email}</p> : null}