Our services

If you can think it, we can make it brainsoft.

Making a form accessible without a component library

Written By: BrainSoft In Frontend

Component libraries hand you a working form and hide the details. That is fine until you need a custom layout, a design system that does not match, or you just want to ship a small bundle. Then you write the markup yourself and inherit every accessibility decision that the library used to make for you.

You can build an accessible form with plain HTML and a small amount of JavaScript. The core is: a real <label for> on every control, a <fieldset> with a <legend> for grouped inputs, aria-describedby pointing at hint and error text, and a live region that announces errors when they appear. This post walks through the exact markup and the few lines of script that tie it together.

Start with labels, not placeholders

A placeholder is not a label. It disappears on input, it is often low contrast, and screen readers treat it inconsistently. Every control gets a visible <label> tied by for and id. If the design calls for a hidden label, hide it with a clip technique rather than display:none, so assistive tech still reads it.

<div>
  <label for="email">Email address</label>
  <input id="email" name="email" type="email"
         autocomplete="email" required
         aria-describedby="email-hint">
  <p id="email-hint">We only use this to send the receipt.</p>
</div>

The autocomplete attribute matters more than people expect. It lets password managers and mobile keyboards do the right thing, which is an accessibility win for anyone using voice input or switch control.

Group related inputs with fieldset and legend

Radio buttons and checkboxes that belong together need a group name that is announced once, not repeated on every item. That is what <fieldset> and <legend> are for. Do not fake it with a heading and aria-labelledby unless you have a reason; the native element works everywhere.

<fieldset>
  <legend>Preferred contact method</legend>
  <label><input type="radio" name="contact" value="email"> Email</label>
  <label><input type="radio" name="contact" value="phone"> Phone</label>
</fieldset>

Wrapping the input inside the label gives you an implicit association, which is fine and saves an id. Just do not mix both patterns on the same control.

Errors: describe, mark, and announce

An error message has to do three things. It must be programmatically associated with the field, it must be visible, and it must be announced when it appears. The first two are markup. The third needs a live region.

Render the error text in the DOM with an id, and point aria-describedby at it. When the field is invalid, add aria-invalid="true" so screen readers say so. Keep the message in the DOM even before validation if the layout allows, or inject it and update the description. A common mistake is colour alone: red border, no text. Do not do that.

<input id="email" name="email" type="email"
       aria-describedby="email-error"
       aria-invalid="true">
<p id="email-error">Enter an email address like name@example.com.</p>

For the announcement, put a single empty live region near the top of the form and write into it on submit. One region is easier to manage than one per field, and it avoids a screen reader reading five errors in a row when only the first one matters.

<p id="form-status" role="status" aria-live="polite"></p>

On submit, if validation fails, focus the first invalid field and set the status text to something like "2 fields need attention". Moving focus is what actually helps keyboard users; the live region tells them why focus moved.

Keyboard, focus, and the small stuff

  • Do not remove focus outlines. Style them instead with :focus-visible.
  • Buttons submit with Enter, so do not intercept Enter on inputs to do something clever.
  • Keep the tab order matching the visual order. Avoid positive tabindex values.
  • Disable the submit button only when you also explain why, or skip disabling and validate on submit.
  • After a successful submit, move focus to the confirmation message and give it role="status".

None of this is exotic. It is the behaviour browsers already give you when you use native elements and stop fighting them with divs and click handlers. If your team is rebuilding a design system or auditing a checkout flow, our services cover front-end work and accessibility reviews.

Frequently asked questions

Do I still need ARIA if I use native elements?

Less than you think. Native <input>, <label>, <fieldset> and <button> carry the roles and states already. Use ARIA for what HTML cannot express, like aria-describedby for hints and errors, or aria-invalid after validation. The first rule of ARIA is to prefer the native element.

How do I make errors announce without being annoying?

Use one polite live region for the form, not one per field, and only write to it on submit or on blur after a failed attempt. Avoid aria-live="assertive" for routine validation; it interrupts whatever the user is reading. Pair the announcement with a focus move to the first invalid field so the user lands where the problem is.

Is client-side validation enough on its own?

No. Client-side validation is a convenience for the user, not a security control. Anyone can skip it with dev tools or a direct request. Validate again on the server, return clear errors, and render them into the same markup so the accessible behaviour holds on both paths.


#Frontend