HTML Formatterhtmlformatteronline.com

Contact form

A contact form with real labels, inline validation and error and success states.

formcontactvalidationinput
<form class="form" id="contact" novalidate>
  <h2>Send us a message</h2>
  <p class="intro">We reply within two working days.</p>

  <div class="field">
    <label for="name">Your name</label>
    <input type="text" id="name" name="name" autocomplete="name" required
           aria-describedby="name-err">
    <p class="err" id="name-err" hidden>Please tell us your name.</p>
  </div>

  <div class="field">
    <label for="email">Email address</label>
    <input type="email" id="email" name="email" autocomplete="email" required
           aria-describedby="email-hint email-err">
    <p class="hint" id="email-hint">We only use this to reply.</p>
    <p class="err" id="email-err" hidden>That does not look like an email address.</p>
  </div>

  <div class="field">
    <label for="topic">What is this about?</label>
    <select id="topic" name="topic">
      <option>A question about an order</option>
      <option>Returns and exchanges</option>
      <option>Wholesale</option>
      <option>Something else</option>
    </select>
  </div>

  <div class="field">
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="5" required
              aria-describedby="message-err"></textarea>
    <p class="err" id="message-err" hidden>Please write a message.</p>
  </div>

  <label class="check">
    <input type="checkbox" name="consent" required aria-describedby="consent-err">
    <span>I am happy for Acme to store this message in order to reply.</span>
  </label>
  <p class="err" id="consent-err" hidden>We need your agreement before we can store the message.</p>

  <!-- honeypot: real people never fill this in -->
  <div class="pot" aria-hidden="true">
    <label for="website">Website</label>
    <input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
  </div>

  <button class="submit" type="submit">Send message</button>

  <p class="status" id="status" role="status"></p>
</form>

How it works

Every input has a real label. Not a placeholder standing in for one. Placeholders disappear the moment someone starts typing, which means anyone who is interrupted loses the context, and low-contrast placeholder text fails contrast requirements in most implementations. Labels also give you a much larger click target, since clicking a label focuses its field.

novalidate is deliberate. It switches off the browser's own bubble messages so you control the wording and the styling. The required and type="email" attributes stay on the fields — they still inform assistive technology and mobile keyboards.

Validation timing matters. Validating on every keystroke tells someone their email is invalid while they are still typing the first letter. This validates on blur, and then re-validates on input only for a field that has already failed — so the error clears as soon as they fix it, without ever appearing prematurely.

Focus moves to the first invalid field on a failed submit. Without that, a keyboard or screen reader user has no idea which of six fields is the problem.

The honeypot catches most bots at zero cost to real users. Bots fill in every field they find; the field is positioned off-screen, excluded from the tab order and hidden from assistive technology. If it comes back filled, the submission is silently discarded — telling a bot it failed only helps it improve.

Accessibility notes

aria-describedby links each field to its hint and error text, so a screen reader reads the error along with the field rather than leaving it stranded.

aria-invalid marks the field itself as failing, and doubles as the CSS hook for the red border — so the visual and programmatic states can never drift apart.

The status message uses role="status", which announces changes politely without interrupting whatever the user is doing.

Errors are never signalled by colour alone: each has an exclamation mark and a sentence explaining what to fix.

Making it yours

Replace the fake delay in the submit handler with a real request. For a static site, Formspree, Netlify Forms and Basin all accept a plain POST and need no server of your own.

The autocomplete attributes are worth keeping and extending. name, email, tel and street-address let browsers fill forms in one tap, which measurably improves completion on mobile.

Related templates

Check your version

Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.