Chapter 8: Building Accessible Forms Without a Framework

How to create real-world, inclusive forms with just HTML and CSS and no JavaScript bloat


Forms are where your users take action.
They’re also where accessibility breaks the most.

The biggest irony in frontend today?
We reach for entire frameworks to build forms… that HTML already knows how to do.

alt text

This chapter will teach you how to build accessible, semantic, and responsive forms without libraries just by using native HTML and a bit of CSS knowledge.


Section 8.1: The Anatomy of a Proper Form

Every form has three essential parts:

  1. Field: input, select, textarea, etc.
  2. Label: explains what the input is for
  3. State: required, invalid, described by help text, etc.

A complete example:

<form>
  <label for="email">Email</label>
  <input
    id="email"
    name="email"
    type="email"
    required
    aria-describedby="email-help"
  />
  <small id="email-help">We'll never share your email.</small>

  <button type="submit">Subscribe</button>
</form>

This form:

No React needed. No schema. Just semantics.


Section 8.2: Why label Is Non-Negotiable

If you skip <label>, screen readers can’t see it. Even if you think a placeholder is enough, it’s not.

<label for="username">Username</label>
<input id="username" type="text" />

OR:

<label>
  Username
  <input type="text" />
</label>

Both work.

Avoid styling with display: none;. Use .visually-hidden so labels are still read by assistive tech.


Section 8.3: Use fieldset and legend for Groups

Grouping related inputs (like checkboxes or radios)? Use fieldset.

<fieldset>
  <legend>Choose your plan</legend>

  <label><input type="radio" name="plan" /> Basic</label>
  <label><input type="radio" name="plan" /> Premium</label>
</fieldset>

This gives screen readers a grouped context, not just a list of isolated options.


Section 8.4: Validations Without JavaScript

Use HTML5’s built-in attributes:

The browser handles validation, shows messages, and prevents submission without a single line of JS.

You can style invalid fields with :invalid and :valid.

input:invalid {
  border: 2px solid red;
}

Section 8.5: Describe Help Text and Errors with ARIA

Use aria-describedby to link help text or error messages to fields.

<input id="zip" aria-describedby="zip-help" />
<small id="zip-help">Enter your 5-digit ZIP code.</small>

Screen readers will read both the input label and the help text giving full context.


Section 8.6: Keyboard Navigation Best Practices

Avoid:

Bonus: Use tabindex="-1" on alert messages so you can focus them with JS when needed.


Section 8.7: Visual Tips Without Breaking Accessibility

Use :focus-visible:

input:focus-visible {
  outline: 2px solid blue;
}

Only shows focus ring for keyboard users.

Use :has() for parent styling:

.form-group:has(input:invalid) {
  border-color: red;
}

(Chrome & Safari only)


Section 8.8: Forms That Just Work on Mobile


Summary

alt text

You don’t need a form library to build a great form; you just need to respect what HTML already does well.


Resources


Coming Up Next

In Chapter 9, we’ll explore real-world responsive design with layout patterns that scale from mobile to desktop, no frameworks or grid systems required.