Home Blog Accessibility Best Practices for Jekyll Themes (WCAG 2.2 Guide)
Tutorial

Accessibility Best Practices for Jekyll Themes (WCAG 2.2 Guide)

Build accessible Jekyll themes that meet WCAG 2.2 standards — semantic HTML, keyboard navigation, colour contrast, ARIA labels, skip links, and automated testing.

Accessibility Best Practices for Jekyll Themes (WCAG 2.2 Guide)

Accessibility is not a nice-to-have — it determines whether your content reaches everyone. An inaccessible Jekyll theme excludes users with visual, motor, and cognitive disabilities, and it signals to search engines that your site is lower quality. This guide covers the practical changes that move a Jekyll theme from unusable to WCAG 2.2 AA compliant.


Why Accessibility Matters for Jekyll Sites

  • Legal: WCAG 2.1 AA is a legal requirement for public-facing sites in the EU, UK, US (ADA), and many other countries
  • SEO: Accessible HTML is semantically rich HTML — the same practices that help screen readers help search engines
  • Reach: 15% of the global population has some form of disability; poor accessibility excludes them
  • Quality signal: Google’s page quality guidelines explicitly include accessibility as a factor

1. Semantic HTML Structure

The single most impactful accessibility change is using the right HTML elements.

Use Landmark Elements

<!-- Wrong -->
<div class="header">...</div>
<div class="nav">...</div>
<div class="main">...</div>
<div class="footer">...</div>

<!-- Right -->
<header class="site-header">...</header>
<nav class="site-nav" aria-label="Main navigation">...</nav>
<main id="main-content" class="site-main">...</main>
<footer class="site-footer">...</footer>

Screen reader users navigate by landmark. Without <header>, <nav>, <main>, <footer>, they can’t jump to sections.

Heading Hierarchy

One <h1> per page. Then <h2> for major sections, <h3> for subsections:


<!-- _layouts/default.html -->
<!-- The page title is always the H1 -->

<!-- _layouts/post.html -->
<h1 class="post-title">{{ page.title }}</h1>

<!-- Post content headings start at H2 -->
<!-- In Markdown: ## Section → <h2>, ### Subsection → <h3> -->

In _config.yml, configure kramdown to handle heading anchors:

kramdown:
  auto_ids: true

Use <article>, <section>, <aside>


<article class="post">          <!-- Self-contained content -->
  <header class="post-header">  <!-- Intro of the article -->
    <h1>{{ page.title }}</h1>
  </header>
  <section class="post-content"> <!-- Thematic section -->
    {{ content }}
  </section>
  <footer class="post-footer">  <!-- Metadata, tags -->
    ...
  </footer>
</article>

<aside class="sidebar" aria-label="Sidebar"> <!-- Supplementary content -->
  ...
</aside>


2. Keyboard Navigation

Every interactive element must be reachable and operable with a keyboard alone — no mouse required.

Add a skip link as the very first element in <body>. Users who navigate by keyboard can skip the nav and jump straight to content:

<!-- In _layouts/default.html, first element inside <body> -->
<a href="#main-content" class="skip-link">Skip to main content</a>

<header>...</header>
<main id="main-content">...</main>
.skip-link {
  position: absolute;
  top: -100%;
  left: 1rem;
  background: var(--color-primary);
  color: #fff;
  padding: 0.5rem 1rem;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  font-weight: 600;
  text-decoration: none;
  z-index: 999;
  transition: top 0.2s;

  &:focus {
    top: 0;
  }
}

This link is invisible until a keyboard user tabs to it — then it appears at the top of the screen.

Focus Styles

Never remove focus outlines:

// Wrong
*:focus { outline: none; }
*:focus-visible { outline: none; }

// Right — enhance the focus style instead of removing it
*:focus-visible {
  outline: 3px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: 2px;
}

:focus-visible shows the outline only for keyboard navigation, not mouse clicks — so it doesn’t feel cluttered for mouse users.

Mobile Navigation Toggle

<button class="nav-toggle" 
        aria-controls="main-menu"
        aria-expanded="false"
        aria-label="Open navigation menu">
  <span aria-hidden="true"></span>
</button>

<nav id="main-menu" aria-label="Main navigation">
  ...
</nav>
const toggle = document.querySelector('.nav-toggle');
const menu = document.querySelector('#main-menu');

toggle.addEventListener('click', () => {
  const isOpen = toggle.getAttribute('aria-expanded') === 'true';
  toggle.setAttribute('aria-expanded', String(!isOpen));
  // Show/hide menu
});

The aria-expanded attribute tells screen readers whether the menu is open or closed.


3. Colour Contrast

WCAG 2.2 AA requires:

  • Normal text (< 18pt): minimum 4.5:1 contrast ratio against background
  • Large text (≥ 18pt or 14pt bold): minimum 3:1 contrast ratio
  • UI components (buttons, inputs, focus indicators): minimum 3:1

Check Your Colours

Use WebAIM Contrast Checker or the browser DevTools accessibility panel.

Common failures in Jekyll themes:

  • Light grey text on white: #999 on #fff = 2.85:1 (fails AA)
  • Placeholder text in forms: often too light
  • Disabled button text
// Common fixes
$color-text-muted: #6b7280;    // 4.6:1 on white — just passes AA
$color-text-muted: #595959;    // 7:1 on white — passes AAA, safer

// Never use lighter than #767676 for body text on white

Dark Mode Contrast

Check both light and dark mode. A common mistake is fixing contrast in light mode and breaking it in dark mode:

:root {
  --text-muted: #6b7280;     // 4.6:1 on #fff ✓
}

[data-theme="dark"] {
  --text-muted: #9ca3af;     // 4.6:1 on #1f2937 ✓ — check this independently
}

4. Images and Alt Text

Every <img> needs an alt attribute. Its value depends on the image’s purpose:

<!-- Meaningful image — describe what it shows -->
<img src="jekyll-setup.png" alt="Terminal showing Jekyll server starting on port 4000">

<!-- Decorative image — empty alt, so screen readers skip it -->
<img src="divider.svg" alt="" role="presentation">

<!-- Image that IS the content of a link — describe the destination -->
<a href="/themes/chirpy/">
  <img src="chirpy-preview.jpg" alt="Chirpy Jekyll theme preview">
</a>

In Jekyll templates, use front matter for alt text:


{% if post.image %}
  <img src="{{ post.image | relative_url }}" 
       alt="{{ post.image_alt | default: post.title }}"
       loading="lazy"
       width="800" height="450">
{% endif %}


5. Forms

<form>
  <!-- Every input needs a visible, programmatically-associated label -->
  <div class="form-group">
    <label for="email">Email address</label>
    <input type="email" id="email" name="email" 
           required
           aria-describedby="email-hint"
           autocomplete="email">
    <p id="email-hint" class="form-hint">We'll never share your email.</p>
  </div>

  <!-- Don't use placeholder as a label substitute -->
  <!-- Placeholders disappear when typing — users forget what the field is for -->

  <!-- Error messages -->
  <input type="email" 
         aria-invalid="true"
         aria-describedby="email-error">
  <p id="email-error" role="alert" class="form-error">
    Please enter a valid email address.
  </p>

  <button type="submit">Subscribe</button>
</form>

<!-- Links must describe their destination -->
<!-- Wrong -->
<a href="/themes/minimal-mistakes/">Click here</a>

<!-- Right -->
<a href="/themes/minimal-mistakes/">View Minimal Mistakes theme</a>

<!-- When context makes it clear, aria-label can supplement -->
<a href="/themes/chirpy/" aria-label="View Chirpy Jekyll theme details">
  Chirpy
</a>

<!-- Buttons must describe their action -->
<!-- Wrong -->
<button onclick="toggleMenu()"></button>

<!-- Right -->
<button onclick="toggleMenu()" aria-label="Open navigation menu"></button>

<!-- Don't use <a> for actions that aren't navigation -->
<!-- Wrong -->
<a href="#" onclick="submitForm()">Submit</a>

<!-- Right -->
<button type="submit">Submit</button>

7. Automated Testing

axe DevTools

Install the axe DevTools browser extension — it scans any page and reports WCAG violations with severity levels and fix guidance.

htmlproofer

Check for missing alt text and broken links as part of your build:

bundle exec htmlproofer ./_site \
  --checks Images,Links \
  --disable-external \
  --ignore-empty-alt

Lighthouse

Google’s Lighthouse (built into Chrome DevTools → Lighthouse tab) includes an Accessibility audit. Aim for 90+ score.

# Or run from CLI
npx lighthouse https://yourdomain.com --only-categories=accessibility

Manual Testing

Tools catch about 30% of accessibility issues. Also test:

  • Tab through the entire page with keyboard only — can you reach everything?
  • Test with a screen reader: NVDA (Windows, free), VoiceOver (Mac, built-in)
  • Zoom browser to 200% — does layout break?
  • Test in forced colours mode (Windows High Contrast)

Quick Wins Checklist

  • lang attribute on <html>: <html lang="en">
  • Skip link to main content
  • <main id="main-content"> landmark
  • One <h1> per page
  • All images have alt attributes
  • All form inputs have associated <label> elements
  • Focus styles visible (not outline: none)
  • Text contrast 4.5:1 minimum
  • Interactive elements reachable by keyboard
  • aria-label on icon-only buttons
  • aria-current="page" on active nav link
  • Lighthouse accessibility score 90+

Accessible themes are better themes — cleaner HTML, better SEO, and a wider audience. Browse Jekyll themes on JekyllHub and check the theme’s accessibility score before choosing.


References

Share LinkedIn