How to build WCAG-compliant Django templates
The WebAIM Million project tests the top million homepages every year. In 2021, they found an average of 51.4 accessibility errors per page. That's per homepage, not per site.
Django developers have an advantage. The framework encourages structure. Templates live in one place. Forms handle validation. The admin interface is generated from models. That structure makes accessibility easier to implement and harder to break.
But Django doesn't write accessible templates for you. You have to do that.
Start with semantic HTML, not divs
Screen readers depend on HTML that tells them what things mean. A <button> is a button. A <nav> element contains navigation. Headings from <h1> to <h6> create an outline of your content.
The Wagtail HTML guidelines, which apply to any Django project using Wagtail or not, state two basic principles: write valid HTML and write semantic HTML. They target the HTML5 doctype .
Here's what semantic HTML looks like in practice. Instead of this:
html
<div class="header"> <div class="logo">...</div> <div class="nav"> <div class="nav-item">Home</div> <div class="nav-item">About</div> </div> </div>
You write this:
html
<header> <div class="logo">...</div> <nav aria-label="Main"> <ul> <li><a href="/">Home</a></li> <li><a href="/about/">About</a></li> </ul> </nav> </header>
The difference isn't visual. It's structural. A screen reader user can jump directly to the navigation. They know how many items are in the list. They hear "list, two items" before you read the links.
The Django documentation site itself made these improvements. They wrapped their table of contents in <nav aria-label="Documentation table of contents">. They did the same for the previous/next links at the bottom of pages. Small changes, big impact for people using assistive technology .
Form fields need labels, always
Django's form system makes it easy to render fields. {{ form.as_p }} and {{ form.as_table }} generate labels automatically. That's good.
But if you're rendering forms manually, you have to include labels. Every input needs one. Not placeholders. Placeholders disappear when users type. They're not a substitute.
The Django admin's recent keyboard shortcuts project, django-admin-keyshortcuts, had to consider form interactions carefully. The team added shortcuts like / to focus the search bar and Ctrl+s to save objects. Those shortcuts only work if form fields are properly labeled and focusable .
Here's a accessible pattern for a Django form field:
html
<div class="field-wrapper"> {{ form.email.label_tag }} {{ form.email.errors }} {{ form.email }} {% if form.email.help_text %} <p class="help" id="{{ form.email.auto_id }}_helptext">{{ form.email.help_text }}</p> {% endif %} </div>
The label_tag method generates the correct <label> element with the for attribute pointing to the input's ID. That's the baseline. Without it, screen reader users don't know what to type.
Dropdowns need clear blank options
A Django ticket from October 2024 (#35870) addressed something specific: blank options in select dropdowns. Marijke Luttekes filed the ticket, and James Scholes, who is blind and an accessibility expert, provided feedback .
The issue was what the blank option should say. When a dropdown has no initial value, Django shows a blank option. But what text should that option have?
James Scholes recommended "(select an option)". The parentheses matter. They disrupt first-letter keyboard navigation as little as possible. If someone is typing to jump through options, the parentheses don't interfere like other punctuation might.
The ticket is still open, but the lesson is clear. Small details matter. The text inside an option affects how screen reader users navigate. Default Django behavior might not be optimal, and you may need to customize.
If you're building forms with select dropdowns, check what your blank option says. Consider whether it's clear to someone who can't see the other options yet.
Keyboard navigation is not optional
Some users can't use a mouse. They tab through interfaces. They use keyboard shortcuts. Your templates need to support that.
The Wagtail CMS team has been working on accessibility checks built into the CMS itself. They're using Axe, the same engine that powers many accessibility testing tools, to flag issues as editors create content . One thing they check is keyboard focus. Can you reach every interactive element?
A. Rafey Khan spent summer 2025 working on keyboard shortcuts for Django admin through Google Summer of Code. His package, django-admin-keyshortcuts, adds:
/to focus searchj/kto focus next or previous objectCtrl+sto saveAlt+dto prompt delete
The shortcuts come with a dialog so users can discover them. That's crucial. Adding shortcuts no one knows about doesn't help.
But Khan also found limitations. Some shortcuts don't work in Safari. Modifier keys behave differently across browsers. The team may need to wait for fixes in the underlying hotkey library . The trade-off is that cross-browser keyboard support is harder than it looks.
If you're building custom Django templates, test them with keyboard only. Unplug your mouse. Tab through every link, button, and form field. Can you reach everything? Do you know where you are? If not, your templates need work.
ARIA: use it sparingly or not at all
WAI-ARIA attributes can fix accessibility problems, but they can also create them. The Wagtail HTML guidelines reference the ARIA Authoring Practices and specifically note: "No ARIA is better than Bad ARIA" .
This is from the W3C itself. Adding ARIA incorrectly can override native semantics and make things worse for screen reader users.
If your button is a <button>, you don't need role="button". That's already there. If your navigation is a <nav>, you don't need role="navigation". Native HTML wins.
Use ARIA when you're building something HTML can't express. A tab panel. A modal dialog. A live region that updates without page reload. Those cases need ARIA. For everything else, use the right HTML element.
The Django docs site added ARIA labels to their navigation regions, like aria-label="Documentation table of contents" . That's appropriate because it disambiguates multiple nav elements. Without it, screen reader users would hear "navigation, navigation" and not know which is which.
Test with real tools, not just hope
You can't guess whether your templates are accessible. You have to test.
The Wagtail team is building accessibility checks directly into the CMS. They're using Axe and storing results over time to show trends. The goal is to give editors a score and help them improve content as they create it .
For Django developers, several tools exist:
- WAVE from WebAIM. Free browser extensions that show errors visually.
- Axe from Deque. Integrates with browser dev tools and can run in CI.
- Curlylint and djhtml. These lint Django templates specifically for HTML issues. Wagtail uses them .
- NVDA on Windows or VoiceOver on Mac. Free screen readers. Use them.
The AccChecky project, built in a hackathon, used the WAVE API to check sites and display results with charts . That's one approach. The simpler approach is just running WAVE on your pages and fixing what it finds.
One example: building an accessible navigation
Let's walk through a real Django template for a main navigation menu.
First, the base template might include a block for navigation:
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}My Site{% endblock %}</title> </head> <body> <a href="#main" class="skip-link">Skip to main content</a> <header> <nav aria-label="Main"> <ul> <li><a href="/" {% if request.path == '/' %}aria-current="page"{% endif %}>Home</a></li> <li><a href="/about/" {% if request.path == '/about/' %}aria-current="page"{% endif %}>About</a></li> <li><a href="/services/" {% if request.path == '/services/' %}aria-current="page"{% endif %}>Services</a></li> <li><a href="/contact/" {% if request.path == '/contact/' %}aria-current="page"{% endif %}>Contact</a></li> </ul> </nav> </header> <main id="main"> {% block content %}{% endblock %} </main> </body> </html>
This does several things:
lang="en"tells screen readers what language to pronounce.- The skip link lets keyboard users bypass the navigation.
<nav aria-label="Main">identifies the navigation region.aria-current="page"tells screen reader users which page is current.<main id="main">provides the skip link target.
That's all HTML. No ARIA magic. Just using elements correctly.
The overlay shortcut and why to avoid it
There's a PyPI package called django-all-in-one-accessibility. It installs a widget that adds an accessibility toolbar to your site. Users can change contrast, increase text size, and use a screen reader .
It installs in two minutes. It costs nothing. It seems like a quick fix.
It's not a fix.
Overlays have fundamental problems. They add JavaScript that modifies the page after it loads. They can conflict with users' own assistive technology. They give the illusion of accessibility without fixing the underlying code. The Wagtail guidelines about "No ARIA is better than Bad ARIA" apply doubly to overlays.
If you use an overlay and call your site accessible, you're misleading yourself and your users. The overlay doesn't fix missing alt text. It doesn't fix heading structure. It doesn't make your forms labelable. It just adds a toolbar.
Build accessible templates instead. It's more work. It's also the only approach that actually works.
What Django is doing internally
The Django project itself has been working on accessibility. The keyboard shortcuts package came out of GSoC 2025 and may eventually merge into core . The ticket about select dropdown blank options is open and being discussed . The documentation site has been updated with better semantic HTML .
Django's own admin interface, which thousands of developers use daily, is getting attention. Keyboard shortcuts make it usable for people who can't use a mouse. Better form labeling helps everyone.
If you're building Django templates, you can follow the same trajectory. Start with semantic HTML. Label your forms. Test with keyboard and screen reader. Fix what's broken.
The tools you actually need
You don't need expensive enterprise tools to build accessible Django templates. You need:
- A code editor
- A browser
- The WAVE extension
- A screen reader for testing
- Curlylint or djhtml for linting
Wagtail's UI guidelines recommend djhtml for formatting Django templates and Curlylint for linting them. They also recommend running make lint to check your code .
Curlylint checks for things like missing alt text on images, improper heading order, and missing form labels. It runs against your templates, not your rendered HTML, so you catch issues early.
Add it to your CI pipeline. Run it on every pull request. Stop merging code that fails basic accessibility checks.
The trade-off: time vs. lawsuits
Building accessible templates takes time. You have to learn the rules. You have to test. You have to fix things that seem fine visually but fail for screen reader users.
The alternative is getting sued.
The ADA doesn't have a technical standard for websites, but courts look to WCAG. WCAG 2.1 Level AA is the de facto benchmark. If your site doesn't meet it, you're exposed.
For Django developers, the choice is clear. Spend time now building it right. Or spend money later on lawyers and settlements.
Django templates are just HTML with some template tags. HTML has accessibility built in. Use it.
Write semantic HTML. Use <nav>, <main>, <header>, <footer>. Label your forms. Make sure keyboard users can reach everything. Add skip links. Test with real assistive technology.
The Django community is working on this. The admin is getting keyboard shortcuts. The documentation is getting better. Tickets about select dropdowns and blank options are being discussed .
You can wait for Django to solve all these problems for you. Or you can solve them yourself in your templates now. One approach leaves you vulnerable. The other makes your site work for more people.

Comments (0)
No comments yet.