Skip to main content

EAA-Compliant Contact Form: A Step-by-Step Guide for Web Developers

Information about the article

Portrait of Dmitry Dugarev wearing glasses in a black shirt and smiling

Author: Dmitry Dugarev

Consultant for digital accessibility & IT compliance

Last updated on:

The European Accessibility Act (EAA) [1] is in force and brings digital accessibility into focus. For you as a developer, this means: Every element, especially a contact form, must be usable by absolutely everyone. An inaccessible form is not only a legal risk but also a missed opportunity to connect with potential customers [2].

In this guide, we will build a form step-by-step that meets current legal requirements. I will explain not only what needs to be done but also why, referencing official guidelines. By the end, you will have a robust, accessible contact form that you can use in your projects.

Here is the live demo of the finished, accessible contact form:

Demo Contact Form

All fields marked with an asterisk () are required. When a message is submitted, if there are no errors, a success message will appear, but nothing further will happen.

Your personal data
A valid email address consists of a name, the "@" symbol, and a domain (e.g., john.doe@example.tld).
For any follow-up questions. Only valid German mobile numbers with 7-11 digits (e.g., 01701234567).
Preferred contact method

Basic Structure & Instructions

The basis of every accessible form is clean, semantic HTML. This starts with correctly linking labels (<label>) with their input fields (<input>).

Every field needs a permanently visible <label> [3], which is connected to the field's id via the for attribute [4]. Placeholders that disappear upon input are no substitute for a label [5]. In Figure 1.1 you can see the example structure of an accessible form, which we will build below.

Furthermore, you should place general instructions, such as marking required fields, before the form so that users know what to expect [6]. The labels of the input fields should also be positioned before the field [7].

Open text description for "Schematic Basic Structure of an Accessible Form"

This diagram shows the basic building blocks of a form.

  1. Contact Form: The outermost element that encloses everything. It contains instructions, an error summary, fieldsets, and a send button.
  2. Field Groups (<fieldset>): Used for grouping related input fields. It contains a label (<legend>) and the actual input fields.
  3. Input Fields: Each field consists of a label (<label>) linked to an input field (<input>). Additionally, there may be input help that provides further information about the field.

Here is a code example for the basic structure:

HTML: Basic Structure of the Form
<p id="form-instructions-demo">
All fields marked with an asterisk (*) are required fields.
</p>

<div
id="error-summary-demo"
class="contact-form__error-summary"
role="alert"
tabIndex="-1"
hidden
aria-labelledby="error-summary-title-demo"
>
<h2 id="error-summary-title-demo">Your entries contain errors</h2>
<p id="error-summary-intro"></p>
<ul class="contact-form__error-summary-list">
</ul>
</div>

<form id="contact-form-demo" class="contact-form__form" novalidate>
<div class="contact-form__group">
<label for="fname-demo" class="contact-form__label">
<span class="contact-form__error-prefix" hidden>Error: </span>
First Name*
</label>
<input
type="text"
id="fname-demo"
name="fname"
required
aria-required="true"
aria-invalid="false"
aria-describedby="fname-error-demo"
class="contact-form__input"
/>
<div
id="fname-error-demo"
class="contact-form__error-message"
hidden
></div>
</div>
<button type="submit" class="contact-form__submit-button">
Send Message
</button>
</form>

We use novalidate in the <form> tag to disable the browser's inaccessible default validation and manage error handling ourselves.

Focus Order and Visibility for Keyboard Operability

A form that cannot be operated without a mouse is not accessible. Two criteria are essential here:

  1. Logical Focus Order: Users navigating with the Tab key must be able to move through the form in a logical and intuitive sequence [9]. With a clean HTML structure (top to bottom), this is usually automatic. More complex layouts (e.g., using CSS Grid or Flexbox) must be checked for this.

  2. Visible Focus: Every interactive element (input, button, link) must show a clearly visible indicator when it receives focus [10]. This is the "you are here" indicator for keyboard users. Never hide the outline style without providing a better, clearly visible replacement.

Logical Grouping of Input Fields

Related information (such as "First Name" and "Last Name" or a group of radio buttons) must also be programmatically recognizable as a group [11]. You use <fieldset> and <legend> for this [12] or alternatively ARIA roles such as role="group" [13].

The <fieldset> encloses the group, and the <legend> gives it a name that screen readers read aloud [12]. Without this structure, users of assistive technologies must guess which fields form a thematic block.

HTML: Grouping Fields with <fieldset>
<fieldset class="contact-form__fieldset">
<legend class="contact-form__legend">Your personal data</legend>

<div class="contact-form__group">
<label for="fname-demo" class="contact-form__label">
<span class="contact-form__error-prefix" hidden>Error: </span>
First Name*
</label>
<input
type="text"
id="fname-demo"
name="fname"
required
aria-required="true"
aria-invalid="false"
aria-describedby="fname-error-demo"
class="contact-form__input"
/>
<div id="fname-error-demo" class="contact-form__error-message" hidden></div>
</div>

<div class="contact-form__group">
<label for="lname-demo" class="contact-form__label">
<span class="contact-form__error-prefix" hidden>Error: </span>
Last Name*
</label>
<input
type="text"
id="lname-demo"
name="lname"
required
aria-required="true"
aria-invalid="false"
aria-describedby="lname-error-demo"
class="contact-form__input"
/>
<div id="lname-error-demo" class="contact-form__error-message" hidden></div>
</div>
</fieldset>

<fieldset
class="contact-form__fieldset"
aria-describedby="contactMethod-group-error-demo"
>
<legend id="contactMethod-legend" class="contact-form__legend">
<span class="contact-form__error-prefix" hidden>Error: </span>
Preferred contact method*
</legend>
<div class="contact-form__radio-group">
<label for="contact-pref-email-demo" class="contact-form__radio-label">
<input
type="radio"
id="contact-pref-email-demo"
name="contactMethod"
value="email"
required
aria-required="true"
class="contact-form__radio-input"
/>
By Email
</label>
<label for="contact-pref-phone-demo" class="contact-form__radio-label">
<input
type="radio"
id="contact-pref-phone-demo"
name="contactMethod"
value="phone"
required
aria-required="true"
class="contact-form__radio-input"
/>
By Phone
</label>
</div>
<div
id="contactMethod-group-error-demo"
class="contact-form__error-message"
hidden
></div>
</fieldset>

Help Texts & Input Support

Sometimes fields need additional explanations, e.g., about a specific format. These help texts must be programmatically linked to the field.

For this, the help text receives an id and the <input> element receives an aria-describedby attribute pointing to this id [14]. A screen reader then reads the label first, followed by the help text.

To further facilitate input, you should use the autocomplete attribute [15]. It allows the browser to automatically suggest known data (name, email, etc.). This is not only convenient but also an explicit requirement [16].

HTML: Help Texts and Autocomplete
<div class="contact-form__group">
<label for="phone-demo" class="contact-form__label">
<span class="contact-form__error-prefix" hidden>Error: </span>
Mobile Phone Number (Optional)
</label>
<input
type="tel"
id="phone-demo"
name="phone"
autocomplete="tel"
aria-invalid="false"
aria-describedby="phone-hint-demo phone-error-demo"
class="contact-form__input"
placeholder="z.B.: 01701234567"
/>
<span id="phone-hint-demo" class="contact-form__help-text">
For any inquiries. Only valid German mobile phone numbers.
</span>
<div id="phone-error-demo" class="contact-form__error-message" hidden></div>
</div>

<div class="contact-form__group">
<label for="email-demo" class="contact-form__label">
<span class="contact-form__error-prefix" hidden>Error: </span>
Email Address*
</label>
<input
type="email"
id="email-demo"
name="email"
autocomplete="email"
required
aria-required="true"
aria-invalid="false"
aria-describedby="email-hint-demo email-error-demo"
class="contact-form__input"
/>
<span id="email-hint-demo" class="contact-form__help-text">
A valid email address (e.g. max.mustermann@example.com).
</span>
<div id="email-error-demo" class="contact-form__error-message" hidden></div>
</div>

Error Handling & Validation

Errors happen. An accessible form guides the user precisely toward correction [18]. The browser's default validation (which we disabled with novalidate) is unsuitable for this because its messages are often not focusable or are not noticed by screen readers [19].

Our approach consists of three parts:

  1. Error Summary: A <div> at the beginning of the form that lists all errors [6]. This helps users quickly see what needs to be corrected and allows them to jump directly to the erroneous fields [20].
  2. Inline Error: A text message directly beneath the erroneous field to provide the user with detailed information [21]. The input field itself is marked with aria-invalid="true" so that screen readers recognize the error [22].
  3. Focus Management: The focus is actively set to the error summary using JavaScript when an error occurs.
  4. We adjust the <label> (or <legend>) of each erroneous field by adding an "Error: " prefix. This is crucial to meet WCAG Success Criterion 1.4.1 [23]. If we only changed the color, the error would not be recognizable to colorblind users.

In Figure 4.1 you can see the logic of validation and error handling that we will implement below.

Open text description for "Logic of Accessible Error Handling"

This flow chart shows the validation process:

  1. The user clicks "Send".
  2. JavaScript prevents the default submit action.
  3. The resetErrors() function deletes all old error messages.
  4. The validateForm() function checks all fields.
  5. A decision checks: "Errors found?".
  6. If Yes: The showErrorSummary() function populates the error summary, showInlineError() marks the fields, and the focus is set on the error summary via errorSummary.focus().
  7. If No: The form is submitted.

Now we build the logic and the HTML structure for error handling.

The error summary must be prepared in the HTML. The summary receives role="alert", so screen readers immediately read it when displayed [25], and tabindex="-1", so we can focus it via JavaScript [26].

HTML: Preparing the Error Summary
<div
id="error-summary-demo"
class="contact-form__error-summary"
role="alert"
tabindex="-1"
hidden
aria-labelledby="error-summary-title-demo"
>
<h2 id="error-summary-title-demo">Your entries contain errors</h2>
<p id="error-summary-intro"></p>
<ul class="contact-form__error-summary-list"></ul>
</div>

<!-- Form fields -->

In this example, the error summary is initially hidden with hidden and aria-hidden="true". As soon as an error occurs, it is made visible via JavaScript.

The accessible error handling is now implemented!

Making the Submission Status (Loading) Accessible

After a user clicks "Send" and the form is valid, the submission process begins. This network request takes a moment. If you don't provide feedback here, the user is uncertain:

  • "Did something happen?"
  • "Do I need to click again?"

We must bridge this uncertain "in-between" accessibly. The best method is to disable the button to prevent double submission [27] and communicate a "busy" status.

We adjust the else block in our submit event listener accordingly.

JavaScript: Disable Submit Button and Announce Status
// ... inside the submit event listener ...

// ... (Variable Definitions)
const submitButton = form.querySelector('button[type="submit"]');

if (Object.keys(newErrors).length > 0) {
// ... (Error handling as above) ...
} else {
// NO ERRORS: Submit form

// 1. Set UI status to 'submitting'
// This function controls the button, spinner, and live region
updateUIforStatusChange('submitting');

// 2. Your actual submission logic (e.g., fetch) goes here
// In this example, we simulate it:
simulateFormSend() // This function returns a Promise
.then((response) => {
// Handle success case
updateUIforStatusChange('success');
})
.catch((error) => {
// Handle error case
updateUIforStatusChange('error');
});
}
// ...

/**
* Simulates sending (takes 3 sec.)
*/
function simulateFormSend() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ success: true });
}, 3000);
});
}

For the status announcement (statusRegion.textContent = ...), we need a live region. You can place this directly next to your error summary in the HTML. It is important to use role="status" and aria-live="polite" so that the announcement does not interrupt the current screen reader output [28].

HTML: Live Region for Loading and Success Status
<div
id="form-status-message"
role="status"
aria-live="polite"
class="visually-hidden"
>
<!-- Here the status is displayed -->
</div>

Status After Submission (Success/Error)

After loading, there are two results: success (HTTP 200) or error (e.g., server error, HTTP 500). You must clearly communicate both to the user. There are two common, accessible scenarios here.

This is the most robust and simplest method.

  1. On Success: Redirect the user to a "Thank You" page (e.g., /contact/thanks).
  2. On Error: Redirect the user to an "Error" page (e.g., /contact/error).

Here is an example of the success redirection in handleSuccess():

JavaScript: Success Redirection
function handleSuccess(response) {
// 1. Success redirection
window.location.href = '/contact/thanks';
}

For this to be accessible, the new page must:

  • Have a clear, descriptive <title> tag (e.g., "Thank you for your message - My Website").
  • Have a clear <h1> heading (e.g., "Thank you!").
  • The focus is automatically set to the beginning of the document when the new page loads, causing the screen reader to read the new page title and the <h1>.

You do not need aria-live or focus management for this, as the browser page change already clearly communicates the context switch.

Conclusion

Creating an accessible form is not witchcraft, but solid craftsmanship. It requires care and an understanding of how different people interact with the web.

By using semantic HTML (label, fieldset), creating clear structures, and implementing fault-tolerant, supportive user guidance, you not only meet the legal requirements of the EAA [1]. You build a better, more user-friendly product for all your users [29]. The techniques shown here are universally applicable and a decisive step towards a truly inclusive internet.

A consolidated checklist of all points from this guide can be found in my Form Checklist for Web Developers.

Frequently Asked Questions (FAQ) about Accessible Forms

Why use novalidate in the <form> tag?

The novalidate attribute disables the browser's default error messages. These are often not accessible (e.g., only visible as small tooltips that disappear after a short time) and are not uniformly designed [19]. With novalidate, we take full control and can implement robust error handling with JavaScript that is accessible to everyone.

Is a red border sufficient for error messages?

No. A red border is purely visual information and is not perceivable for users with visual impairments (e.g., color blindness) or when using a screen reader [23]. Accessible error handling requires programmatic states (like aria-invalid="true") [22] and text alternatives that can be read out by assistive technologies [18].

Must I follow WCAG 2.1 or 2.2?

The EAA currently refers to WCAG 2.1 [30] via the EU standard EN 301 549 [31]. However, WCAG 2.2 [32] is the newer, recommended standard. It is a best practice to already orient yourself toward WCAG 2.2, as this is future-proof and fully covers the requirements of 2.1. You can find more information in my section "Legal Situation regarding Digital Accessibility".

What is the difference between required and aria-required="true"?

required is an HTML5 attribute that tells the browser a field must be filled (and triggers native validation if novalidate is missing). aria-required="true" is an ARIA attribute that explicitly tells assistive technologies (like screen readers) in the most robust way that a field is required. It is best to use both to ensure maximum compatibility and semantic clarity, because in some cases required alone is not correctly interpreted by all technologies or ARIA is not supported.

What about the click size of buttons (Target Size)?

An important change in WCAG 2.2 is Criterion 2.5.8 Target Size (Minimum) (Level AA) [33]. It states that interactive elements (buttons, links, radio buttons, checkboxes) should not be smaller than a minimum size of 24x24 CSS pixels.

This is crucial for users with motor impairments, e.g., on touch devices or those with unsteady hands, as it reduces accidental clicking on the wrong element. Ensure in your CSS that your click targets (especially the often too-small radio buttons and checkboxes) meet this requirement.

Disclaimer

The content of this guide is for informational purposes only and does not constitute legal advice. While I strive to provide accurate and current information, I make no warranty regarding the completeness, accuracy, or timeliness of the information provided. Compliance with accessibility standards may vary depending on the specific context and use case. It is recommended that you seek professional advice when implementing accessible forms and conduct regular tests with real users to ensure that the requirements of the WCAG 2.2 guidelines are met. I am not liable for any damages or losses resulting from the use of or reliance on the information contained in this guide.

  1. Bundesministerium der Justiz und für Verbraucherschutz, “Gesetz zur Umsetzung der Richtlinie (EU) 2019/882 des Europäischen Parlaments und des Rates über die Barrierefreiheitsanforderungen für Produkte und Dienstleistungen (Barrierefreiheitsstärkungsgesetz – BFSG).” 2023. [Online]. Available: https://www.gesetze-im-internet.de/bfsg/
  2. Forrester Research, “Accessibility Is Still Vital For Businesses.” Feb. 03, 2025. [Online]. Available: https://www.forrester.com/blogs/accessibility-is-still-vital-for-businesses/
  3. W3C, “Success Criterion 3.3.2 Labels or Instructions,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#labels-or-instructions
  4. W3C, “H44: Using label elements to associate text labels with form controls,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/html/H44
  5. W3C, “F82: Failure of Success Criterion 3.3.2 by visually formatting a set of phone number fields but not including a text label,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/failures/F82
  6. W3C, “G83: Providing text descriptions to identify required fields that were not completed,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/general/G83
  7. W3C, “G162: Positioning labels to maximize predictability of relationships,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/general/G162
  8. W3C, “Success Criterion 1.4.10 Reflow,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#reflow
  9. W3C, “Success Criterion 2.4.3 Focus Order,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#focus-order
  10. W3C, “Success Criterion 2.4.7 Focus Visible,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#focus-visible
  11. W3C, “Success Criterion 1.3.1 Info and Relationships,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#info-and-relationships
  12. W3C, “H71: Providing a description for groups of form controls using fieldset and legend elements,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/html/H71
  13. W3C, “ARIA17: Using grouping roles to identify related form controls,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA17
  14. W3C, “ARIA1: Using the aria-describedby property to provide a descriptive label for user interface controls,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA1
  15. W3C, “H98: Using HTML autocomplete attributes,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/html/H98
  16. W3C, “Success Criterion 1.3.5 Identify Input Purpose,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#identify-input-purpose
  17. W3C, “Success Criterion 1.4.3 Contrast (Minimum),” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#contrast-minimum
  18. W3C, “Success Criterion 3.3.1 Error Identification,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#error-identification
  19. E. Smith, “Accessible form validation with examples and code,” Pope Tech, Oct. 2025, [Online]. Available: https://blog.pope.tech/2025/09/30/accessible-form-validation-with-examples-and-code/
  20. W3C, “G139: Creating a mechanism that allows users to jump to errors,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/general/G139
  21. W3C, “G85: Providing a text description when user input falls outside the required format or values,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/general/G85
  22. W3C, “ARIA21: Using aria-invalid to Indicate An Error Field,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA21
  23. W3C, “Success Criterion 1.4.1 Use of Color,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#use-of-color
  24. W3C Web Accessibility Initiative (WAI), “W3C Forms Tutorial: User Notifications.” 2023. [Online]. Available: https://www.w3.org/WAI/tutorials/forms/notifications/
  25. W3C, “ARIA19: Using ARIA role=alert or Live Regions to Identify Errors,” 2023, [Online]. Available: https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA19
  26. WHATWG, “HTML Living Standard.” 2024. [Online]. Available: https://html.spec.whatwg.org/
  27. W3C, “Success Criterion 3.3.4 Error Prevention (Legal, Financial, Data),” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#error-prevention-legal-financial-data
  28. W3C, “Success Criterion 4.1.3 Status Messages,” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#status-messages
  29. W3C Web Accessibility Initiative, “The Case for Digital Accessibility.” 2022. [Online]. Available: https://www.w3.org/WAI/business-case/
  30. W3C, “Web Content Accessibility Guidelines (WCAG) 2.1.” 2021. [Online]. Available: https://www.w3.org/TR/WCAG21/
  31. ETSI, “ETSI EN 301 549 V3.2.1 (2021-03) - Accessibility requirements for ICT products and services.” 2021. [Online]. Available: https://www.etsi.org/deliver/etsi_en/301500_301599/301549/03.02.01_60/en_301549v030201p.pdf
  32. W3C, “Web Content Accessibility Guidelines (WCAG) 2.2.” 2023. [Online]. Available: https://www.w3.org/TR/WCAG22/
  33. W3C, “Success Criterion 2.5.8 Target Size (Minimum),” 2023, [Online]. Available: https://www.w3.org/TR/WCAG22/#target-size-minimum

About the author

Portrait of Dmitry Dugarev wearing glasses in a black shirt and smiling

Best regards,

Dmitry Dugarev

Founder of Barrierenlos℠ and developer of the Semanticality™ plugin. With a master’s degree, over 8 years of experience in web-development & IT-Compliance at Big Four, Bank and Enterprise, and more than 1,000 web pages tested for accessibility for over 50 clients, I help web teams implement accessibility in a structured way — without months of redevelopment.

Become an EAA Expert Developer yourself - in just 4 days.

Learn in our 4-day workshop how to conduct complex audits and develop accessible components yourself. Get a personalized learning plan, 1:1 strategy coaching, and a certificate.

Register for the EAA Workshop now