Codeskill

Learn to code, step by step

Forms in depth (grouping, labels, validation attributes, helpful errors)

A deeper look at HTML forms: grouping related fields, labels that work, built-in validation attributes, and error messages that actually help. The beginner series covered basic inputs and a simple <form>; here we focus on patterns you will use on contact pages, checkout flows, and sign-up screens.

Form structure recap

A form wraps controls that collect user input and submit it somewhere – usually a server endpoint via method="post" or method="get". Every control that should be submitted needs a name attribute. Buttons submit the form unless you set type="button" for actions that should not.

Start with a sensible action and method, then build the fields inside:

<form action="/contact/submit" method="post">
  <!-- fields -->
  <button type="submit">Send message</button>
</form>

Server-side validation is still mandatory. HTML validation is a first line of defence and a convenience for users, not a security boundary.

Grouping with fieldset and legend

Related fields belong in a <fieldset> with a <legend> that names the group. Screen readers announce the legend when focus enters the group. Sighted users see a visual boundary (once CSS styles it).

Typical uses: radio button sets, checkbox groups, address blocks, payment details.

<fieldset>
  <legend>Preferred contact method</legend>

  <p>
    <label>
      <input type="radio" name="contact" value="email" checked>
      Email
    </label>
  </p>
  <p>
    <label>
      <input type="radio" name="contact" value="phone">
      Phone
    </label>
  </p>
</fieldset>

Do not nest fieldsets unnecessarily. One legend per logical group is enough. For a long form, split into several fieldsets (‘Your details’, ‘Your enquiry’) rather than one giant box.

Labels done properly

Every input needs an accessible name. The best way is a <label> associated with the control. You can wrap the input (as above) or use for and id:

<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" required>

Placeholder text is not a label. It disappears when the user types and often fails contrast checks. Use a real label, always.

When a field is required, say so in the label or nearby text – not with colour alone:

<label for="name">Full name <span aria-hidden="true">(required)</span></label>
<input id="name" name="name" type="text" autocomplete="name" required>

The aria-hidden="true" on ‘(required)’ avoids some screen readers announcing ‘required’ twice when the field also has the required attribute. Adjust if your testing shows otherwise – the goal is clear, non-repetitive announcements.

Help text and descriptions

Short hints (‘We will never share your email’) sit well in a plain paragraph below the label. Link them to the input with aria-describedby so assistive tech reads the hint after the label:

<label for="password">Password</label>
<input id="password" name="password" type="password"
       aria-describedby="password-hint" minlength="8" required>
<p id="password-hint">At least 8 characters.</p>

Keep hints short. If you need a long explanation, link to a separate help page rather than cluttering the form.

HTML validation attributes

Browsers can validate common rules before submit. Useful attributes:

  • required – field must have a value
  • type="email", type="url", type="tel" – basic format checks
  • minlength / maxlength – character limits on text
  • min / max / step – bounds on numbers and dates
  • pattern – regular expression the value must match

Example contact form fragment:

<fieldset>
  <legend>Your details</legend>

  <p>
    <label for="c-name">Name</label>
    <input id="c-name" name="name" type="text" autocomplete="name" required>
  </p>

  <p>
    <label for="c-email">Email</label>
    <input id="c-email" name="email" type="email" autocomplete="email" required>
  </p>

  <p>
    <label for="c-message">Message</label>
    <textarea id="c-message" name="message" rows="6" minlength="20" required></textarea>
  </p>
</fieldset>

autocomplete helps browsers fill known values correctly. Use tokens from the WHATWG spec (name, email, street-address, and so on) rather than inventing your own.

Pattern with care

pattern takes a regular expression matched against the whole value. It is easy to get wrong and hard for users to understand when validation fails. Use it when a simple type is not enough – UK postcode format, account reference with fixed structure – and document the expected format in visible text.

<label for="postcode">Postcode</label>
<input id="postcode" name="postcode" type="text"
       pattern="[A-Za-z]{1,2}[0-9][A-Za-z0-9]? ?[0-9][A-Za-z]{2}"
       aria-describedby="postcode-format" required>
<p id="postcode-format">For example SW1A 1AA</p>

Postcode regex is notoriously fiddly. Test with real examples and expect to adjust. Server-side validation should still normalise and verify.

Helpful error messages

Browser default validation messages vary and can be vague (‘Please fill out this field’). For important forms, you will eventually handle errors in JavaScript or server-rendered HTML. Even before that, you can improve the basics.

Use title sparingly on inputs – some browsers incorporate it into validation tooltips, but it is not reliable everywhere. Better: set custom messages with JavaScript via the Constraint Validation API (setCustomValidity()), or render server errors next to each field after submit.

Server-side error pattern (after failed submit):

<p>
  <label for="c-email">Email</label>
  <input id="c-email" name="email" type="email"
         value="not-an-email"
         aria-invalid="true"
         aria-describedby="c-email-error" required>
  <span id="c-email-error" role="alert">Enter an email address like name@example.com.</span>
</p>

Key points:

  • aria-invalid="true" marks the field as failed validation
  • aria-describedby links the error text so screen readers announce it
  • role="alert" on the error can prompt immediate announcement when injected dynamically
  • The message says what went wrong and how to fix it – not just ‘Invalid’

noreferrer and form security (briefly)

Forms that change data should use method="post". Sensitive actions belong on HTTPS. Autocomplete attributes help password managers; they are not a security feature. Never rely on client-side validation alone for anything that matters.

If a form uploads files, set enctype="multipart/form-data" on the <form> – easy to forget, impossible to fix with attributes on the file input alone.

Submit and secondary actions

One clear primary submit button per form. Secondary actions (‘Clear’, ‘Cancel’) use type="button" or link-styled anchors that navigate away without submitting:

<button type="submit">Send message</button>
<button type="reset">Clear form</button>

Reset buttons annoy users who click them by accident. Many production forms omit reset entirely. Cancel is often a link back to the previous page.

Testing checklist

  • Tab through the form with keyboard only. Can you reach every control and submit?
  • Turn off CSS. Is the grouping and label association still clear?
  • Submit empty and invalid values. Are errors visible and linked to fields?
  • Try a screen reader or browser accessibility inspector on one fieldset and one error state.

Next: custom form controls – styling checkboxes, building toggle buttons, and keeping accessibility intact when the default browser widgets are not enough.

PreviousLandmark regions and page templates (header, nav, main, aside, footer)