Building better software, one stack at a time.

Web Development

How to Build a Fast, Accessible Contact Form with HTML, CSS and Vanilla JavaScript

How to Build a Fast, Accessible Contact Form with HTML, CSS and Vanilla JavaScript

A contact form carries more weight than it looks. When it works, nobody notices. When it fails, you lose the message and the person who sent it. Building one that loads quickly, explains itself to assistive technology and survives a keyboard-only visitor is less about clever code and more about restraint. The approach here is straightforward: write semantic HTML that works on its own, style it without breaking focus, then add validation in vanilla JavaScript as an enhancement rather than a crutch.

Start with semantic HTML that stands alone

Before any scripting, the form should submit and be readable by a screen reader. Get the markup right first and everything after it becomes easier.

  • A form element with a real action and method, so a browser with JavaScript disabled still sends something to your server.
  • One control per piece of information, with a type that matches the data: email, tel, text, and a textarea for the message body.
  • A label for every control, tied to it with matching for and id attributes.
  • Sensible autocomplete values such as name, email and tel, so browsers and password managers can fill the form for the visitor.
  • A submit button whose text describes the action. "Send message" beats "Submit" when a page has more than one form.

Keep the native constraints too: required, type="email", maxlength and a pattern where it genuinely helps. Without JavaScript, the browser enforces them, and you have a working baseline.

Once your script loads, call form.noValidate = true (or set the novalidate attribute) so your own messages take over. Otherwise visitors see two rounds of complaints: the browser bubble and then yours, which feels broken.

Labels, hints and error slots

Placeholder text is not a label. It disappears the moment someone starts typing, and it is often the only clue about what a field wants. Put a visible label above each input, then use the placeholder, if at all, for an example like "[email protected]".

Build error messages into the markup as empty elements with unique ids, and point to them with aria-describedby on the input. An input might describe both a hint and an error: aria-describedby="email-hint email-error". Leave the error element empty until it is needed, then fill it with a short sentence. "Enter an email address in the format [email protected]" tells someone what to do. "Invalid input" does not.

If you group radio buttons or checkboxes, wrap them in a fieldset with a legend. Screen readers announce the legend with every option inside, which is exactly what you want.

Styling that respects focus and contrast

The stylesheet should stay quiet. A single column, a max-width around 60 characters, and grid or flexbox with a consistent gap handles the layout for almost any form.

  • Set input font-size to at least 16px. Anything smaller makes iOS Safari zoom in when the field is focused, which throws the layout around.
  • Never remove the focus outline without replacing it. A 2–3px solid outline with a small offset reads clearly against most backgrounds.
  • Give inputs enough padding to reach roughly 44px of height, so touch targets are comfortable.
  • Do not signal errors with red text alone. Add a short message, and consider a border colour change so the problem is obvious even to someone who cannot distinguish the hue.
  • Aim for a contrast ratio of at least 4.5:1 between body text and background, and keep that in mind for disabled and placeholder states too.

Keep any transitions brief and wrap decorative movement in a prefers-reduced-motion query. A form is not the place for animation that competes with the task.

Validation with vanilla JavaScript

Attach one submit handler, prevent the default, and walk through the fields in order. Collect the first invalid control as you go, write your messages into the error slots, set aria-invalid="true" on anything that fails, and move focus to the first problem field at the end. That single focus move saves keyboard users a hunt through the page.

Keep the rules small and obvious: is it empty, is the format plausible, is the message long enough to be useful. For email, a light check for an @ symbol and a dot in the domain is plenty. Elaborate regular expressions reject valid addresses and rarely catch the invalid ones; the real test is whether the message arrives.

Validate on blur, then on input once an error exists

Checking a field the moment someone types the first character is hostile. Validate when focus leaves the field, and clear the error as soon as the visitor starts correcting it. Once a field has failed, re-check it on input so feedback feels immediate. Never validate an empty field on blur if the visitor is simply tabbing past it on the way to somewhere else — wait for a submit attempt before complaining about missing values.

Submitting with fetch without losing the fallback

Send the form with FormData and fetch, and check response.ok before assuming success. Disable the button while the request is in flight and change its text to something like "Sending…", then use a flag to guard against a double submit from an impatient click. If the request fails, keep the visitor's text in the fields and show a message that says what happened and what to do next.

On success, reset the form, write a confirmation into a live region, and move focus there so the announcement is not missed. A polite status region is usually the right choice; the message is not urgent enough to interrupt.

If the form already works without JavaScript, everything you add afterwards is a bonus rather than a dependency.

Test it the way people actually use it

  1. Tab through the entire form, forwards and backwards. Nothing should be reachable only with a mouse, and the order should follow the visual order.
  2. Turn JavaScript off and submit. The plain POST should still reach your endpoint.
  3. Submit an empty form, then a mistyped email. Check the messages are specific and land next to the right field.
  4. Zoom to 200% and set the browser window to about 320px wide. Nothing should overlap or disappear.
  5. Run a screen reader through it — VoiceOver on macOS, NVDA on Windows, or TalkBack on Android. Listen for whether labels and errors are announced once, clearly.
  6. Throttle the connection and watch the submit button. Feedback should appear immediately, even if the network is slow.

Knowing when to stop

The finished form is small: a few dozen lines of markup, a stylesheet that fits on one screen, and validation logic you can read in a sitting. That is the point. Every extra library, animation and clever pattern adds weight to the page and another thing to maintain, while the visitor only wants to type a sentence and press a button.

Ship it, then come back after a month and read the messages that actually arrived. Real submissions tell you which fields confuse people far better than any amount of guessing at a desk.

Photo: viarami / Pixabay

Related Posts

A Beginner's Guide to UK GDPR for Web Developers: Cookies, Consent and Data Requests

Web Development

A Beginner's Guide to UK GDPR for Web Developers: Cookies, Consent and Data Requests

Practical UK GDPR guidance for small websites: how cookie consent really works, what to record, and how to answer...

How to Prepare Your E-commerce Site for the UK Christmas Shopping Rush

Web Development

How to Prepare Your E-commerce Site for the UK Christmas Shopping Rush

A practical guide to load testing, caching, payment limits, stock messaging and calm launch freezes so your shop...