Skip to main content
← All notes
Building

The intake form validates on blur so the error appears before the submit button

softwareaccessibility

Every service page has a contact form in the sidebar. All five — software, audio, production, accessibility, musician — render through the same ServiceIntakeForm component. The form validates each field individually when the client tabs or taps away from it, not when they hit submit. The handleBlur function receives the field config and the current value, runs validateField, and stores the result in an errors state object keyed by field name. A separate touched state object tracks which fields the client has interacted with. A required field that has never been focused does not show an error even though it is empty. The error only appears after the first blur. The client sees what is wrong as they fill in the form, field by field, not in a batch after they think they are done. When an error is present, aria-invalid is set to true on the input and aria-describedby points to the error paragraph's ID. A screen reader announces the problem immediately when focus leaves the field. The error ID is constructed from the field name plus -error so every instance is unique even when multiple fields fail simultaneously. The error paragraph has role alert so assistive technology treats it as a live region and reads it without the client needing to navigate to it. The honeypot check runs before any validation on submit. If the invisible website field has a value, the handler returns silently. No error message, no state change, no fetch call. The bot thinks it submitted. It did not. The submission guard is a useRef, not a useState. A ref read is synchronous — two rapid clicks both see the current value before either can change it. A state read is batched — two clicks could both read false before either update lands. The ref flips to true before the fetch call and back to false in the finally block. The entire form is driven by a FieldConfig array passed as a prop. Each config has a name, label, type, required flag, placeholder, and optional options array for selects. The component maps over the array and renders the right HTML element — input, select, or textarea — based on the type string. Five different service pages pass five different field arrays to the same component. The audio form asks for genre and track count. The software form asks for project type and budget range. Adding a new field to any service means adding one object to the array on that service page. No component change, no API update, no deployment beyond the page file.

Comments coming soon

Sign in with TikTok to leave a comment. Coming soon.