Accessible Contact Forms in HTML and CSS Templates
Contact forms are where many free templates fail basic accessibility checks: fields without labels, placeholders doing a label's job and errors shown only in red. Each of these has a simple HTML fix.
Start with the form element
A contact form in a template preview can look finished while being nearly unusable with a screen reader or a keyboard. The fixes are mostly plain HTML, and they start with the form element itself.
The action attribute names the address that receives the data, and method sets how it is sent. Contact forms should use post, which puts the submitted values in the request body instead of appending them to the URL, where they would end up in browser history and server logs.
<form action="/contact" method="post">
<button type="submit">Send message</button>
</form>
The fields described below go inside the form, before the submit button. Use a real button element rather than a styled link, so the form can be sent with the keyboard.
Some templates add novalidate to switch off the browser's built-in validation messages so a script can show custom ones. Only do that if the custom messages are as accessible as the native ones. A form with novalidate and a half finished script is worse than a form with plain browser messages.
Give every field a real label
Each control needs an accessible name, and the most reliable source is a visible label element connected through for and id:
<div class="field">
<label for="name">Name</label>
<input type="text" id="name" name="name" autocomplete="name" required>
</div>
The connection does two things. Screen readers announce "Name" when the field receives focus, and clicking the label text moves focus into the input, which gives people with limited motor control a larger target.
Placeholders cannot replace labels. The hint vanishes as soon as typing starts, so anyone checking their entries before sending has nothing to compare against. Placeholder text is also usually pale gray, which often fails the WCAG minimum contrast of 4.5:1 for normal text.
If a design truly has no room for visible labels, keep the label in the markup and hide it visually with a utility class:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
Visible labels remain the better choice for most forms.
Choose the right input types
The type attribute changes the on-screen keyboard and enables basic checks. An email field with type="email" brings up a keyboard with the @ key on most phones and lets the browser reject obviously malformed addresses. A phone field with type="tel" shows a number pad.
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email" required>
</div>
<div class="field">
<label for="phone">Phone (optional)</label>
<input type="tel" id="phone" name="phone" autocomplete="tel">
</div>
<div class="field">
<label for="message">Message</label>
<textarea id="message" name="message" rows="6" required></textarea>
</div>
The autocomplete values let browsers fill in saved details, which saves typing and reduces mistakes. Long messages belong in a textarea, which accepts multiple lines.
Mark required and optional fields
Tell people which fields they must fill before they try to submit. You can write "required" in the label, or use an asterisk and explain it once at the top of the form. In many short contact forms it is simpler to make almost everything required and label the one or two exceptions as optional, as in the phone field above.
The required attribute is enough on its own for assistive technology; adding aria-required="true" on top of it is unnecessary.
CSS can style fields according to their state without extra classes:
input:user-invalid,
textarea:user-invalid {
border-color: #b91c1c;
background-color: #fef2f2;
}
The :user-invalid pseudo-class only matches after the person has interacted with the field, so the form does not light up red the moment the page loads. It is supported in all current major browsers.
Write errors people can act on
When something is wrong, say what and how to fix it, in words. Color alone does not count, since a red border means nothing to someone who cannot distinguish red. Place the message next to its field and link the two:
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email"
aria-invalid="true" aria-describedby="email-error">
<p id="email-error" class="error">Enter an email address in the format [email protected].</p>
</div>
Your script should add aria-invalid="true" and the message only after validation fails, and remove them once the value is fixed. With aria-describedby in place, screen readers read the error along with the label when the field receives focus.
For longer forms, show a short summary above the form after a failed submission, with links to each problem field, and move keyboard focus to that summary. A live region with role="alert" will have the summary announced, but it does not move focus, so do that in your script as well.
Stop spam without punishing people
Contact forms attract bots quickly. Image puzzles block a good share of them, but they also block people with visual or cognitive disabilities and annoy everyone else. Several quieter methods work well together.
A honeypot is an extra field hidden from people but present in the markup. Simple bots fill in every field they find, so a value in the honeypot marks the submission as automated:
<div class="hp" aria-hidden="true">
<label for="website">Leave this field empty</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
.hp {
display: none;
}
The tabindex and aria-hidden attributes keep keyboard and screen reader users from landing in the field by accident.
A timing check is the second layer. Record when the form was served, and treat submissions that arrive implausibly fast as suspicious. Rate limiting by address on the server adds a third.
Check everything again on the server
Client-side validation is a convenience for honest users, not a security measure. Anyone can switch it off or send a request straight to the form's address. The script that receives the form has to repeat every check.
That script should confirm the request method, reject anything with a filled honeypot, trim and validate each value, and enforce sensible length limits. If the message is sent by email, never place submitted values in mail headers without removing line breaks, and treat the visitor's address as a reply-to value rather than the sender. If any of it is displayed in a web page later, escape it for HTML first.
Finally, respond in a way people can understand: a clear confirmation page or message on success, and the form returned with their entries intact and the errors explained when something fails. Returning appropriate HTTP status codes as well lets any front-end script tell the two outcomes apart.
Frequently asked questions
Why does every input need a label?
A label gives the field an accessible name that screen readers announce when the field receives focus. Connecting it with matching for and id attributes also makes the label text clickable. A placeholder is not a substitute, because it disappears as soon as someone types.
Is browser validation enough on its own?
No. Attributes such as required and type="email" give quick feedback, but anyone can bypass them. The server that receives the form must check every value again before using it.
Where should error messages appear?
Next to the field they describe, in plain text, linked to the input with aria-describedby. For longer forms, a summary at the top listing each problem helps too.
How can a form reduce spam without a CAPTCHA puzzle?
A hidden honeypot field, a check on how quickly the form was submitted and rate limiting on the server stop a lot of automated spam without asking people to solve anything.