Jamie Software Lab
Home / Engineering / Accessibility
Accessibility WCAG 2.1 Semantic HTML Inclusive

Accessibility Engineering

Accessibility isn't a feature : it's a baseline. Every project I build prioritises keyboard navigation, screen reader support, colour contrast, and semantic markup. If it doesn't work for everyone, it doesn't work.

AA WCAG target
100 Lighthouse score
0 JS Required for content

Accessibility Principles

POUR Framework

WCAG 2.1 is built on four principles. Every accessibility decision I make maps back to one of these:

  • Perceivable : content must be presentable in ways all users can perceive (alt text, captions, contrast)
  • Operable : interface must be navigable via keyboard, voice, and assistive tech
  • Understandable : content and operation must be predictable and readable
  • Robust : content must work across browsers, devices, and assistive technologies

My Approach

Accessibility is built in from the start, not retrofitted:

  • Semantic first : use native HTML elements before reaching for ARIA
  • Progressive enhancement : content works without JS; JS enhances, never gates
  • Test with real tools : screen readers, keyboard-only navigation, and automated audits
  • Continuous compliance : run Lighthouse and axe-core in CI, not just before launch

Semantic HTML Structure

The most impactful accessibility improvement is also the simplest: use the right HTML elements. This portfolio site is a case study in semantic markup - no <div> soup, no ARIA hacks for things native elements handle.

Element Choices and Why

Element Purpose Accessibility Benefit
<header> Site/page header Landmark region : screen readers can jump directly to it
<nav> Navigation links Landmark region with aria-label distinguishing primary vs page nav
<main> Primary content Skip link target; screen readers announce "main content"
<section> Thematic grouping With headings, creates navigable document outline
<article> Self-contained content Screen readers announce as distinct content blocks
<button> Interactive controls Native keyboard support (Enter/Space), focus management, role announcement
<a> Navigation links Native focus, Enter activation, right-click context menu, URL preview
HTML : Semantic structure of this site
<!-- Skip link: first focusable element -->
<a class="skip" href="#content">Skip to content</a>

<!-- Decorative elements hidden from AT -->
<div class="bg" aria-hidden="true">...</div>

<!-- Labelled navigation landmark -->
<header class="topbar">
  <nav aria-label="Primary">
    <a href="/#projects">Projects</a>
    ...
  </nav>

  <!-- Toggle with ARIA state management -->
  <button aria-label="Menu" aria-expanded="false">
    ...
  </button>
</header>

<!-- Main content landmark: skip link target -->
<main id="content">
  <section>
    <h1>Page Title</h1>
    ...
  </section>
</main>

Keyboard Navigation

Every interactive element on this site is reachable and operable via keyboard alone. No mouse required. Here's how keyboard support works across the portfolio:

Keyboard Interaction Map

Key Action Context
Tab Move to next focusable element Global : follows DOM order
Shift + Tab Move to previous focusable element Global : reverse tab order
Enter Activate link or button Links, buttons, nav items
Space Activate button / toggle Buttons, checkboxes
Escape Close mobile nav panel When nav panel is open

Focus Management

  • Visible focus indicators : custom :focus-visible styles with high-contrast outline
  • Logical tab order : follows visual reading order (no tabindex hacks)
  • Skip links : first Tab press reveals "Skip to content" link
  • Focus trapping : mobile nav traps focus when open, returns on close

What I Avoid

  • No tabindex="1+" : positive tabindex breaks natural order
  • No outline: none : focus indicators are never removed
  • No keyboard traps : users can always Tab away from any element
  • No hover-only interactions : everything accessible via keyboard too
CSS : Focus visible styles
/* Only show focus ring for keyboard navigation */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius);
}

/* Skip link: hidden until focused */
.skip {
  position: absolute;
  left: -9999px;
  z-index: 9999;
  padding: 12px 24px;
  background: var(--accent);
  color: #fff;
  border-radius: var(--radius);
}
.skip:focus {
  left: 16px;
  top: 16px;
}

Colour & Contrast

The dark theme isn't just aesthetic : every colour combination meets WCAG AA contrast requirements. I use CSS custom properties to ensure consistency and make compliance auditable.

Contrast Ratios

Combination Ratio WCAG Level Usage
--text on --bg 14.5:1 AAA Primary body text
--muted on --bg 5.8:1 AA Secondary text, labels
--accent on --bg 5.2:1 AA Links, interactive elements
--accent2 on --bg 5.9:1 AA Success states, badges
Code text on code background 10.2:1 AAA Code blocks, inline code

Colour Isn't the Only Signal

Information is never conveyed through colour alone. Status badges include text labels, error messages use icons alongside red text, and links are underlined (not just coloured) to distinguish them.

Dark Theme Considerations

Dark themes are more than inverting colours. I avoid pure black (#000) backgrounds (causes halation), use #070A12 (dark blue-black) instead, and limit text to off-white (#e6e8ef) to reduce eye strain during extended reading.

Screen Reader Support

ARIA Usage

  • aria-label on navigation landmarks
  • aria-hidden="true" on decorative elements
  • aria-expanded on toggle buttons
  • No redundant role attributes on semantic elements

Heading Hierarchy

  • Single <h1> per page
  • No skipped levels (h1 → h2 → h3)
  • Headings describe section content
  • Screen readers build nav from headings

Link Text

  • Descriptive link text (never "click here")
  • External links noted where relevant
  • Same-page anchors for section navigation
  • Consistent naming across pages
HTML : ARIA patterns used
<!-- Decorative: hidden from screen readers -->
<span class="brand__mark" aria-hidden="true"></span>

<!-- Distinguishing multiple nav landmarks -->
<nav aria-label="Primary">...</nav>
<nav aria-label="Page sections">...</nav>

<!-- Toggle state communicated to AT -->
<button
  class="nav-toggle"
  aria-label="Menu"
  aria-expanded="false"
>
  <!-- Visual hamburger lines -->
  <span class="nav-toggle__line"></span>
  <span class="nav-toggle__line"></span>
  <span class="nav-toggle__line"></span>
</button>

<!-- JS updates aria-expanded on toggle -->
<script>
btn.addEventListener('click', function() {
  var open = tb.toggleAttribute('data-nav-open');
  btn.setAttribute('aria-expanded', open);
});
</script>

Responsive & Adaptive Design

Breakpoint Strategy

Breakpoint Target Key Changes
320px Small phones Single column, reduced padding
440px Large phones 2-column card grid
700px Tablets Desktop nav visible
980px Desktop Full 12-column grid
1800px+ Ultrawide Max-width container, centred

Mobile-First Practices

  • Touch targets ≥ 44px : all buttons and links meet minimum tap size
  • No horizontal scroll : content reflows at every viewport width
  • Readable text : minimum 16px font size, no pinch-zoom blocking
  • Reduced motion : prefers-reduced-motion respected for animations
  • Content priority : mobile layout shows most important content first
CSS : Reduced motion & responsive typography
/* Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Fluid typography: scales between breakpoints */
h1 {
  font-size: clamp(2rem, 3.6vw, 3rem);
}

/* No viewport meta blocking zoom */
<!-- ✓ Correct: allows user zoom -->
<meta name="viewport"
  content="width=device-width, initial-scale=1.0">

<!-- ✗ Wrong: blocks accessibility zoom -->
<!-- meta name="viewport"
  content="..., maximum-scale=1, user-scalable=no" -->

Accessibility Testing

Automated

  • Lighthouse : accessibility audit score 100/100
  • axe-core : catches WCAG violations in CI
  • HTML validator : ensures valid markup
  • Contrast checker : verifies colour ratios

Manual

  • Keyboard-only : navigate entire site without mouse
  • Screen reader : test with NVDA and VoiceOver
  • Zoom 200% : verify content reflows correctly
  • Colour blind sim : test with simulated colour blindness

Continuous

  • Pre-commit : lint HTML for a11y issues
  • CI pipeline : axe-core runs on every PR
  • Deploy checks : Lighthouse audit post-deploy
  • Periodic review : manual audit quarterly
100 Lighthouse a11y
0 axe violations
AA WCAG compliance
44px Min touch target