Eleventy (11ty) is a pragmatic static site generator popular with independent developers, government agencies, educational institutions, and content-driven organizations that want fast, framework-light websites. Because Eleventy outputs plain HTML with zero required JavaScript, it has a head start on accessibility: screen readers and keyboard users see exactly what the server rendered, and there is no hydration layer to trap focus or drop announcements. However, Eleventy's flexibility means the authored templates (Nunjucks, Liquid, WebC, or JavaScript) entirely determine accessibility. Teams that copy a starter template often inherit semantic mistakes like empty alt attributes on informative images, unlabeled form controls pulled from a third-party newsletter service, and hardcoded lang="en" on the root layout even when the site ships multilingual content via @11ty/eleventy-plugin-i18n. The eleventy-img plugin and the eleventy-navigation plugin bring specific accessibility concerns of their own: image shortcodes that require manual alt text arguments, navigation plugins that output unstructured lists when the page tree is malformed, and build-time optimizations that strip heading hierarchy from compiled HTML. This checklist walks through the issues that are distinctive to Eleventy projects, from shortcode authoring conventions to data cascade pitfalls to the surprising accessibility consequences of eleventy-plugin-rss. Each item maps to a WCAG 2.1 success criterion and includes a template snippet you can adapt to your own Eleventy configuration.

Common Accessibility Issues

critical

eleventy-img Shortcode Called Without an Alt Argument

WCAG 1.1.1

The official @11ty/eleventy-img plugin exposes a shortcode that accepts src and alt arguments. The plugin explicitly throws when alt is undefined, but many projects wrap it in a custom {% image %} shortcode that passes an empty string as a default fallback. Informative images then ship with alt="" and become invisible to screen readers.

How to fix:

Keep the upstream eleventy-img behavior that throws when alt is missing. For your custom shortcode, require the alt argument explicitly and throw a build error when it is not a non-empty string for non-decorative images. Introduce a separate {% decorativeImage %} shortcode that emits alt="" and role="presentation" for clearly decorative cases so authors have to opt in.

Before
// .eleventy.js
eleventyConfig.addShortcode('image', async function(src, alt = '') {
  let metadata = await Image(src, { widths: [640, 1280] });
  let data = metadata.jpeg[0];
  return `<img src="${data.url}" width="${data.width}" height="${data.height}" alt="${alt}">`;
});
After
// .eleventy.js
eleventyConfig.addShortcode('image', async function(src, alt) {
  if (typeof alt !== 'string' || alt.length === 0) {
    throw new Error(`Missing alt text for ${src}. Use decorativeImage for decoration.`);
  }
  let metadata = await Image(src, { widths: [640, 1280, 1920], formats: ['avif','webp','jpeg'] });
  return Image.generateHTML(metadata, {
    alt,
    sizes: '(max-width: 768px) 100vw, 1280px',
    loading: 'lazy',
    decoding: 'async',
  });
});

eleventyConfig.addShortcode('decorativeImage', async function(src) {
  let metadata = await Image(src, { widths: [640, 1280] });
  return Image.generateHTML(metadata, { alt: '', role: 'presentation', 'aria-hidden': 'true' });
});
serious

Root Layout Hardcoding lang="en" on Multi-Language Sites

WCAG 3.1.1

Eleventy projects serving content in multiple languages (via @11ty/eleventy-plugin-i18n or via directory-based locales) commonly ship a _includes/layouts/base.njk that hardcodes <html lang="en">. Spanish or French pages are then announced in English by screen readers.

How to fix:

Use an eleventyComputed field or the page.lang data cascade to surface the current locale from the directory name or front matter, then render it in the layout via <html lang="{{ page.lang or 'en' }}">. Combine with eleventy-plugin-i18n to resolve the correct locale in URL patterns and inject dir="rtl" for Arabic, Hebrew, and Persian.

Before
<!-- _includes/layouts/base.njk -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>{{ title }}</title>
  </head>
  <body>{{ content | safe }}</body>
</html>
After
<!-- _includes/layouts/base.njk -->
<!DOCTYPE html>
{%- set lang = page.lang or 'en' -%}
{%- set dir = ['ar','he','fa'].includes(lang) ? 'rtl' : 'ltr' -%}
<html lang="{{ lang }}" dir="{{ dir }}">
  <head>
    <title>{{ title }}</title>
  </head>
  <body>{{ content | safe }}</body>
</html>
serious

eleventy-navigation Plugin Outputting Unstructured Menus

WCAG 1.3.1

The eleventy-navigation plugin generates site navigation from page front matter. When pages have inconsistent order or key values, the plugin outputs a flat or disordered list. Screen reader users navigating by landmark arrive at a nav element with semantically incorrect hierarchy, and keyboard users who expect logical tab order get surprised by content jumping between sections.

How to fix:

Audit your navigation output regularly by running the site through a screen reader and confirming the nav element reads in reasonable order. Set an explicit order property on every page with eleventyNavigation front matter, and add an aria-label on the nav element ("Primary", "Footer") to distinguish multiple navs. Use the plugin's activeKey option to mark the current page with aria-current="page".

Before
<!-- nav.njk -->
<nav>
  {%- set navPages = collections.all | eleventyNavigation -%}
  <ul>
    {%- for entry in navPages %}
      <li><a href="{{ entry.url }}">{{ entry.title }}</a></li>
    {%- endfor %}
  </ul>
</nav>
After
<!-- nav.njk -->
<nav aria-label="Primary">
  {%- set navPages = collections.all | eleventyNavigation -%}
  <ul>
    {%- for entry in navPages %}
      {%- set isCurrent = entry.url === page.url -%}
      <li>
        <a href="{{ entry.url }}"{% if isCurrent %} aria-current="page"{% endif %}>{{ entry.title }}</a>
      </li>
    {%- endfor %}
  </ul>
</nav>
critical

Form Partials for Newsletter Embeds Without Labels

WCAG 3.3.2

Eleventy blogs commonly paste Mailchimp, Buttondown, or ConvertKit newsletter signup HTML into a form partial without labels, relying on placeholder text as the only field identifier. Placeholder text is not a programmatic label and disappears when the user starts typing, leaving screen reader users with no context.

How to fix:

Rewrite the newsletter embed partial to emit proper <label for="..."> elements, even if the label is visually hidden with an sr-only class. Add aria-describedby referencing a small help text element and set aria-invalid="true" when the form re-renders with an error. Prefer accessible upstream templates like the Buttondown embed over inline Mailchimp code that ships with decorative styling.

Before
<!-- _includes/partials/newsletter.njk -->
<form action="/subscribe" method="post">
  <input type="email" placeholder="Your email" name="email">
  <button type="submit">Subscribe</button>
</form>
After
<!-- _includes/partials/newsletter.njk -->
<form action="/subscribe" method="post" aria-describedby="nl-help">
  <label for="nl-email">Email address</label>
  <input type="email" id="nl-email" name="email" required aria-required="true" autocomplete="email">
  <p id="nl-help">We send one email per month. Unsubscribe anytime.</p>
  <button type="submit">Subscribe</button>
</form>
serious

Markdown Content Skipping Heading Levels

WCAG 1.3.1

Eleventy projects authored primarily in Markdown often let content authors write ### (h3) as the first heading inside a post body, while the post layout wraps the content in an h1. This skip from h1 to h3 breaks the document outline, and screen reader users pressing H to navigate by heading jump over the skipped level without realizing content is missing.

How to fix:

Pick one convention: the layout owns the h1 and authors start at h2, or authors own the h1 and the layout uses h2 for the site title. Enforce this with a markdown-it plugin or a linter like alex.js or mdl that validates heading hierarchy at build time. Reject pull requests that introduce content with skipped levels.

moderate

eleventy-plugin-rss Generating Feeds Without Alt Text or Language

WCAG 1.1.1

RSS and Atom feeds generated by eleventy-plugin-rss embed the raw Markdown-rendered HTML of posts. If the source Markdown omits alt text, the feed propagates that failure to every RSS reader. The feed XML also commonly omits the <language> element, so accessibility-aware readers like NewsBlur cannot announce the correct locale.

How to fix:

Set the <language> element in your feed template using the same page.lang data cascade that drives the HTML output. Run the same alt-text-validation pass against the rendered feed HTML that you run against the site. Consider generating one feed per locale rather than a single mixed feed.

serious

Focus Styles Stripped by normalize.css-Style Resets in Starter Themes

WCAG 2.4.7

Popular Eleventy starters such as eleventy-base-blog ship with CSS resets that include *:focus { outline: 0 } without providing a replacement focus indicator. Keyboard users cannot see which element has focus, which fails 2.4.7 Focus Visible and 2.4.13 Focus Appearance.

How to fix:

Remove any outline: 0 or outline: none rule that targets :focus without a replacement. Add a :focus-visible rule that shows a high-contrast focus ring (outline: 3px solid accent; outline-offset: 2px; border-radius: 2px). Test the entire tab order on home, post, and form pages after changes to confirm every focusable element shows a visible focus indicator.

Eleventy (11ty)-Specific Tips

  • Use the Image.generateHTML helper from @11ty/eleventy-img rather than hand-rolling the output HTML. It emits srcset, sizes, width, height, loading="lazy", and decoding="async" attributes automatically, which keeps CLS low and satisfies WCAG 1.4.10 Reflow.
  • Add eleventyConfig.setServerOptions({ showAllHosts: true }) during local development so you can test the site from a screen reader running on a different device on the same network (a phone with TalkBack, a tablet with VoiceOver).
  • Install pa11y-ci and run it against the built _site directory in your CI pipeline. Pa11y-ci is fast for static sites and integrates cleanly with Eleventy because the build output is plain HTML that can be served by any static file server.
  • Use WebC components (if you have migrated from Nunjucks) to encapsulate accessibility logic once and reuse it. Components like <card-link> and <icon-button> can centralize aria-label requirements and focus styles in a single file.
  • When embedding third-party scripts (analytics, chat widgets, comments) use loading=lazy iframes or defer loading until after user interaction. Third-party scripts are the single largest source of accessibility regressions on otherwise clean Eleventy sites.

@11ty/eleventy-img

Official Eleventy image plugin that generates optimized responsive images at build time. When configured correctly it emits width, height, srcset, sizes, and loading attributes that satisfy WCAG 1.4.10 Reflow and avoid cumulative layout shift.

Pa11y CI

A command-line accessibility scanner that runs axe-core against a list of URLs and fails the CI build on violations. Works especially well for Eleventy because the build output is static HTML that can be served from any directory and crawled quickly.

eleventy-plugin-validate

Community plugins in the Eleventy ecosystem that validate HTML output, check broken links, and assert accessibility requirements at build time. Combine multiple plugins in .eleventy.js to build a custom compliance gate.

Further Reading

Other CMS Checklists