Craft CMS Accessibility Checklist 2026 | WCAG 2.1 AA & EAA Compliance
Last updated: 2026-04-20
Craft CMS is a developer-focused content management system that gives teams complete control over HTML output through Twig templates, which means accessibility is entirely determined by how developers structure their markup. Unlike website builders that generate semantic HTML automatically, Craft produces exactly what your templates tell it to produce, with no default accessibility safeguards. This control cuts both ways: a well-designed Craft site can meet the strictest WCAG 2.2 AA standards, while a poorly implemented one can ship critical violations that no plugin will fix for you. Craft powers many corporate marketing sites, publications, and ecommerce storefronts across the European Union and United Kingdom, which places most of these sites directly under European Accessibility Act enforcement starting June 2025. Craft also introduces its own accessibility surface in the control panel: the editorial interface, Matrix and Neo field configuration, live preview, and the element indexes your content team uses daily. If editors cannot navigate the control panel with a keyboard, or if field labels are missing, they cannot reliably author accessible content either. This checklist covers accessibility responsibilities on both sides of Craft CMS: the public-facing templates that end users see, and the control panel experience your content team depends on to produce compliant content. Each item references the relevant WCAG 2.1 success criterion and includes Craft-specific remediation guidance using Twig, field configuration, and the Control Panel settings.
Common Accessibility Issues
Craft templates are written by developers from scratch, and many teams copy generic starter templates that use div-heavy layouts without header, nav, main, and footer landmarks. Screen reader users rely on these landmarks to jump directly to navigation or main content. Without them, every page read starts from the top of the DOM and users must hear every menu item before reaching the article.
Audit your _layouts/ templates and replace top-level div wrappers with semantic HTML5 landmarks: header for the site header, nav for primary navigation with an aria-label, main for the primary content region (exactly one per page), aside for complementary content, and footer for the site footer. Ensure child templates extend the layout rather than redefining these landmarks in each template.
{# templates/_layouts/default.twig #}
<div class="site-header">
<div class="nav-wrapper">{% include "_partials/nav" %}</div>
</div>
<div class="page-content">
{% block content %}{% endblock %}
</div>
<div class="site-footer">{% include "_partials/footer" %}</div> {# templates/_layouts/default.twig #}
<header class="site-header">
<nav aria-label="Primary" class="nav-wrapper">{% include "_partials/nav" %}</nav>
</header>
<main id="main" class="page-content">
{% block content %}{% endblock %}
</main>
<footer class="site-footer">{% include "_partials/footer" %}</footer> Craft asset fields let editors upload images with no enforcement of alt text. Many Craft sites bind the raw asset URL to an img tag without any alt attribute, or use the asset title, which is often a filename like "hero-v3-final.jpg". Screen reader users hear the filename or nothing at all, missing informational content.
In the Craft Control Panel, edit your Asset volume and add a required plain text custom field named "Alt text" on the Assets field layout. In your Twig templates, output the alt attribute from this field and never fall back to the filename. For images explicitly marked decorative (add a Lightswitch field named "Decorative"), output alt="" so screen readers skip them. Craft 4.4 and later also include a native alt field on assets; use it rather than rolling your own when available.
{% set image = entry.heroImage.one() %}
{% if image %}
<img src="{{ image.url }}" width="{{ image.width }}" height="{{ image.height }}">
{% endif %} {% set image = entry.heroImage.one() %}
{% if image %}
{% set altText = image.alt ?? image.altText ?? "" %}
<img src="{{ image.getUrl({ transform: "hero" }) }}"
srcset="{{ image.getUrl({ transform: "heroSmall" }) }} 640w, {{ image.url }} 1280w"
width="{{ image.width }}" height="{{ image.height }}"
alt="{{ altText }}"
{% if image.decorative %}role="presentation"{% endif %}>
{% endif %} Craft Matrix and Neo fields let editors assemble pages from modular blocks (hero, text, quote, callout). Most block templates hardcode a heading level like h2 or h3, which means an editor who places a "Quote" block before the first "Section Heading" block can produce a page where an h3 appears before any h2, or where multiple h1s coexist. Screen reader users navigating by heading cannot build a reliable outline.
Pass the current heading level into each block template as a Twig variable (for example startingLevel = 2 after the page h1) and increment it inside the block based on nesting depth. Alternatively, add a "Heading level" dropdown to each block with visual heading options, and render the semantic level from the selected option rather than hardcoding it. Document the hierarchy rules for your content team in the Craft control panel field instructions.
The Craft Control Panel includes Live Preview, slide-outs for editing related elements, and modal dialogs for asset selection. Custom plugin UIs and field types sometimes introduce keyboard traps where focus enters a component but cannot escape with Tab or Escape. Content editors who rely on keyboard navigation may be unable to close a modal or leave a Matrix block without using a mouse.
When building or reviewing custom field types and plugins, test every modal, slideout, and inline editor with keyboard only. Ensure Escape closes any overlay and returns focus to the triggering element, Tab cycles through all interactive elements without leaving the overlay, and focus is moved into the overlay when it opens. Prefer Craft's built-in Garnish components (Garnish.Modal, Garnish.HUD) which handle focus management, rather than rolling custom overlays.
The popular Solspace Freeform plugin and the contact-form.twig starter template generate forms where labels are either missing, visually hidden without being programmatically associated with inputs, or replaced entirely with placeholder text. Required fields often lack aria-required and error messages are not associated with the field that failed validation.
Edit your Freeform or custom form templates so every input has a label element with a for attribute matching the input id. For required fields, add the required attribute and aria-required="true". For error handling, output errors inside a container with aria-describedby linking it to the input, set aria-invalid="true" on invalid fields, and move focus to the first error after a failed submission. Freeform 5 ships accessible default templates called "Bootstrap 5" and "Tailwind"; prefer these over the legacy "Flexbox" and "Grid" defaults.
{% for field in form.layout.fields %}
{{ field.render() }}
{% endfor %} {% for field in form.layout.fields %}
<div class="field {% if field.hasErrors %}has-errors{% endif %}">
<label for="{{ field.idAttribute }}">
{{ field.label }}{% if field.required %} <span aria-hidden="true">*</span><span class="visually-hidden">required</span>{% endif %}
</label>
{{ field.render({
"attributes": {
"input": {
"aria-required": field.required ? "true" : "false",
"aria-invalid": field.hasErrors ? "true" : "false",
"aria-describedby": field.hasErrors ? field.idAttribute ~ "-errors" : null
}
}
}) }}
{% if field.hasErrors %}
<div id="{{ field.idAttribute }}-errors" class="field-errors" role="alert">
{{ field.errors | join(". ") }}
</div>
{% endif %}
</div>
{% endfor %} Craft image transforms are commonly used inside srcset attributes without setting the corresponding width and height attributes on the img element. When the browser lays out the page before images load, the content shifts, which disorients users with low vision, cognitive disabilities, or motor impairments who may lose their place or click the wrong target.
Always output the width and height attributes on img tags, using the transformed asset dimensions so the browser can reserve space. For responsive images with multiple transforms, set width and height to the largest transform's dimensions, and use CSS (max-width: 100%; height: auto) to scale them down for smaller viewports. Craft asset transform objects expose width and height properties for this purpose.
User group permissions in Craft can hide fields from specific roles. Teams sometimes restrict the Alt Text and Caption fields to administrators only, which means day-to-day editors cannot add accessibility metadata when uploading assets. This creates a workflow where every image starts noncompliant and requires admin intervention to be fixed.
Review user group permissions under Settings > Users > User Groups and ensure all roles that upload assets can also edit Alt Text, Caption, and any other accessibility fields on the asset layout. Make these fields required on the field layout so assets cannot be saved without them. Add instructions inside each field explaining what good alt text looks like for your content team.
Craft CMS-Specific Tips
- Use the native asset alt field introduced in Craft 4.4+ rather than creating a custom "Alt text" plain text field. The native field is recognized by plugins like SEOmatic and is surfaced in the asset editor slide-out, making it harder for editors to skip.
- Add field instructions to every content field in your Craft sections. Instructions appear in the control panel and give your content team concrete examples of accessible vs. inaccessible content ("Use descriptive link text like 'Read the 2026 accessibility report', not 'click here'"). This teaches accessibility alongside the authoring task.
- Build your page layouts so editors choose the heading level for major blocks rather than hardcoding it in the template. A "Heading level" dropdown with options "Heading 2", "Heading 3", "Heading 4" is far safer than guessing which level a specific block sits at.
- Use Craft's element API or the built-in preview tokens to run axe-core against your templates in a staging environment before publishing. Pair this with a Craft Utility that reports assets missing alt text so editors can remediate in bulk.
- When building custom Craft plugins that expose UI in the control panel, follow the Garnish JavaScript patterns Craft uses for its own interface. Garnish components ship with focus management, escape-to-close, and aria-live announcements that you otherwise have to implement yourself.
Recommended Tools
Solspace Freeform
The most widely used forms plugin for Craft CMS. Version 5 ships accessible default templates with proper label associations, aria-invalid states, and focus management after submission. Prefer Freeform over building custom forms that reinvent accessibility features.
axe DevTools
A browser extension that runs automated WCAG testing against your rendered Craft templates. Especially valuable for catching missing alt text, insufficient color contrast, and ARIA misuse introduced by Twig template changes.
Craft Accessibility Cleanser by Viget
A Craft plugin that scans entries and assets for common accessibility issues and surfaces them as a Utility in the control panel. Useful for reporting on sites with thousands of assets where manual review is impractical.
Further Reading
Other CMS Checklists
Get our free accessibility toolkit
We're building a simple accessibility checker for non-developers. Join the waitlist for early access and a free EAA compliance checklist.
No spam. Unsubscribe anytime.