How to Build WCAG-Compliant Django Templates

The WebAIM Million project tests the top million homepages each year. In 2021, the average page had 51.4 accessibility errors. Django developers have structural advantages because templates are centralized and the framework encourages clean separation, but Django doesn't write accessible code for you.

Accessible Django templates start with semantic HTML. Use <nav> for navigation, <main> for main content, <header> and <footer> for page regions. Screen readers depend on this structure. Forms need proper labels, not placeholders. Keyboard navigation must work without a mouse.

The Django community is working on accessibility. The django-admin-keyshortcuts project added keyboard shortcuts to the admin in 2025. A ticket about blank options in select dropdowns is open and under discussion. The documentation site now uses ARIA labels to distinguish navigation regions.

Testing requires tools like WAVE, Axe, and screen readers. Automated testing in CI with Curlylint or djhtml catches basic issues. Manual testing with NVDA or VoiceOver catches the rest.

How to Build WCAG-Compliant Django Templates

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 search
  • j/k to focus next or previous object
  • Ctrl+s to save
  • Alt+d to 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.

Frequently Asked Questions

Use semantic HTML. Don't wrap everything in
tags. Use
Every form field needs a proper
There's an open Django ticket (#35870) about this. When a dropdown has no initial value, Django shows a blank option. The question is what text that option should display. James Scholes, an accessibility expert who is blind, recommended "(select an option)". The parentheses matter because they disrupt first-letter keyboard navigation as little as possible. The ticket is still open, so you may need to customize this yourself if the default doesn't work for your users.
Use ARIA sparingly. The W3C's guidance is "No ARIA is better than Bad ARIA." Native HTML elements already have semantics built in. A
Avoid them. Overlays add JavaScript that modifies the page after it loads. They conflict with users' own assistive technology. They give the illusion of accessibility without fixing underlying code like missing alt text or improper heading structure. The Wagtail guidelines reference "No ARIA is better than Bad ARIA," and the same applies to overlays. Build accessible templates instead. It's more work and it actually works.
Use multiple tools. WAVE from WebAIM is a free browser extension that shows errors visually. Axe integrates with browser dev tools and can run in CI. Curlylint and djhtml lint Django templates specifically for HTML issues. Then test with real screen readers. NVDA is free for Windows. VoiceOver is built into every Mac. Turn them on and try to use your own site without looking at the screen. It's revealing.
At minimum, users need to tab through every interactive element and see where they are. That's the baseline. The django-admin-keyshortcuts project added / to focus search, j/k to navigate between objects, Ctrl+s to save, and Alt+d to delete. Those are good patterns. The package also includes a dialog so users can discover the shortcuts. Adding shortcuts no one knows about doesn't help.
Automated tools find about 30% of accessibility issues. They can't tell if a screen reader user can actually navigate your site. They can't evaluate whether your link text makes sense out of context. They can't tell if your heading order is logical. WAVE and Axe are essential starting points, but you must test manually with assistive technology. The Wagtail team is building automated checks into their CMS, but they still recommend manual testing.
Django is working on it. The admin keyboard shortcuts project came out of Google Summer of Code 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 and ARIA labels. Django doesn't write accessible templates for you, but the framework gives you the tools to do it.
Add Curlylint or djhtml to check your templates on every pull request. They catch missing alt text, improper heading order, and missing form labels. Run automated tests with Axe or similar tools against your rendered pages. Stop merging code that fails basic checks. The Wagtail guidelines recommend running make lint and using specific linters to enforce their UI standards. You can do the same.
A skip link is a link at the very top of the page that jumps to the main content. It's usually hidden visually but appears when keyboard users tab to it. Without it, keyboard users have to tab through every navigation link, every header item, every piece of chrome before reaching your content. The pattern is with corresponding id="main" on your main content container. It's simple and required.
Building accessible templates takes more time upfront. You have to learn the rules, test, and fix issues. But it's permanent. Overlays take two minutes to install and then you're still not accessible. The trade-off is time now versus lawsuits later. ADA website lawsuits settle for $5,000 to $15,000 typically, and you can get sued multiple times. A few days of development work costs less than one settlement.
The W3C's WCAG Quick Reference at w3.org/WAI/WCAG21/quickref. WAVE browser extension from WebAIM. Axe DevTools. NVDA screen reader. Curlylint on GitHub. The Django documentation site itself for examples of accessible navigation and structure. The django-admin-keyshortcuts repository for keyboard shortcut patterns.

Ready to Find Your Perfect Vehicle?

Browse our extensive inventory or schedule a test drive today!

Janeth

About Janeth

Comments (0)

No comments yet.

Get More Info