Skip to main content

Making nataliewalls.com More Accessible: A Devlog

· By Natalie Walls

Making nataliewalls.com More Accessible: A Devlog

I ran an accessibility audit on nataliewalls.com using Skynet Technologies' Free Accessibility Checker. The results? Some work to do.

This is a devlog documenting the fixes as I implement them.

The Audit Results

The report showed:

  • 20 passed readability checks
  • 25 failed readability checks
  • 1 failed title check: "Page must contain a <h1> element"
  • Several "Not Applicable or May Require Manual Audit" items

The failed H1 check was confusing because I did have an H1 tag on the page. Turns out, accessibility checkers look for proper semantic structure, and I had some issues.

Fix #1: Proper Semantic HTML Structure

Problem: The root layout wasn't wrapping content in a <main> element, and some pages had nested <main> tags.

Solution: Added a single <main> element in the root layout with an ID:

<main id="main-content">
  {children}
</main>

Changed nested <main> elements in child pages to <section> elements to avoid invalid HTML.

Fix #2: Skip-to-Content Link

Problem: Keyboard users had no way to skip repetitive navigation and jump straight to the main content.

Solution: Added a visually hidden skip link that appears on focus:

<a 
  href="#main-content" 
  className="skip-to-content"
  onClick={(e) => {
    e.preventDefault();
    const main = document.getElementById('main-content');
    if (main) {
      main.setAttribute('tabindex', '-1');
      main.focus();
      main.removeAttribute('tabindex');
    }
  }}
>
  Skip to main content
</a>

With CSS:

.skip-to-content {
  position: absolute;
  left: -9999px;
  z-index: 999;
  padding: 1em;
  background-color: #000;
  color: white;
  text-decoration: none;
  border: 2px solid white;
}
.skip-to-content:focus {
  left: 0;
  top: 0;
}

Update: The first version used href="#main-content" which caused the browser to navigate, sometimes closing the tab. Fixed by preventing default and manually focusing the element with JavaScript.

Update 2: Since Next.js App Router requires metadata exports at the root layout level, and event handlers can't be passed to Server Components, I extracted the skip link into a separate Client Component (skip-to-content.tsx). This keeps the root layout as a Server Component while allowing the skip link to have interactive behavior.

Now keyboard users can press Tab once and hit Enter to skip to the main content without any navigation issues.

Fix #3: ARIA Labels for Links

Problem: Links that open in new tabs don't announce this to screen reader users. External links without context can be confusing.

Solution: Added descriptive aria-label attributes to all external links:

<a 
  href="https://github.com/n8m8" 
  target="_blank" 
  rel="noopener noreferrer"
  aria-label="Visit Natalie's GitHub profile (opens in new tab)"
>
  GitHub →
</a>

Same treatment for project cards:

<a 
  href="https://jobjob.dev" 
  target="_blank" 
  rel="noopener noreferrer"
  aria-label="Visit JobJob - AI Resume and Job Search tool (opens in new tab)"
>
  {/* card content */}
</a>

Fix #4: Better Focus Indicators

Problem: Default browser focus indicators can be subtle or inconsistent across browsers.

Solution: Added explicit focus styles for all interactive elements:

a:focus, button:focus {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

This gives keyboard users a clear visual indicator of where they are on the page.

Fix #5: Meta Tags and Document Structure

Problem: Missing viewport meta tag and charset declaration (Next.js adds these by default, but it's good to be explicit).

Solution: Added proper meta tags to the <head>:

<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

Also added box-sizing: border-box to the global CSS reset for consistent sizing behavior.

Fix #6: Color Contrast Improvements

Problem: The audit flagged 25 readability issues, mostly related to insufficient color contrast. Text using #666 gray on white backgrounds doesn't meet WCAG AA standards (4.5:1 contrast ratio minimum).

Solution: Improved contrast across the site:

  • Body text: #333#1a1a1a (near black)
  • Secondary text: #666#555 (darker gray)
  • Footer text: #999#555
  • Tag badges: #f0f0f0 background with #666 text → #e0e0e0 with #1a1a1a text
  • Border colors: #eee#ddd (more visible)

These changes bring all text to WCAG AA compliance while maintaining the clean, minimal aesthetic.

Fix #7: About Section Accessibility

Problem: The "About" section (bio) had no semantic structure for screen readers. It was just paragraphs with no heading.

Solution: Added semantic structure with a visually hidden heading:

<section style={{ marginBottom: '40px', lineHeight: 1.7 }} aria-label="About Natalie">
  <h2 className="sr-only">About</h2>
  <p>I build things with code...</p>
</section>

The .sr-only class hides the heading visually but keeps it available for screen readers:

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Now screen reader users can navigate by headings and understand the structure of the page.

Fix #8: Blog List Page Improvements

Problem: The blog listing page had the same contrast and semantic issues as the homepage.

Solution: Applied the same fixes:

  • Added aria-label to the blog posts section
  • Used proper <time> elements with dateTime attributes
  • Added aria-label to each blog post link describing what it does
  • Added unique id attributes to each post title for aria-labelledby
  • Improved color contrast for all text and borders

Example:

<article aria-labelledby={`post-title-${post.slug}`}>
  <h2 id={`post-title-${post.slug}`}>
    <Link 
      href={`/blog/${post.slug}`}
      aria-label={`Read article: ${post.title}`}
    >
      {post.title}
    </Link>
  </h2>
  <time dateTime={post.date}>
    {new Date(post.date).toLocaleDateString('en-US', {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    })}
  </time>
  <p>{post.description}</p>
</article>

This creates a clear relationship between the article, its heading, and its link for assistive technology users.

Testing

To verify these fixes:

  1. Keyboard navigation: Tab through the entire site, ensure focus is visible and order makes sense
  2. Screen reader: Test with VoiceOver (macOS) or NVDA (Windows) to confirm links and structure are announced correctly
  3. Re-run audit: Check if the H1 error is resolved and if new issues appear

Learning to Use VoiceOver (The Hard Way)

Here's something I didn't expect: testing accessibility is harder than building it.

I made all these fixes based on automated audits and best practices, feeling pretty confident. Then I turned on VoiceOver to test... and had no idea what I was doing.

My assumptions (all wrong):

  • Tab navigation would be enough ❌
  • Arrow keys alone would navigate content ❌
  • It would be intuitive to use ❌

The reality:

  • Tab navigation only focuses interactive elements (links, buttons). It's supposed to skip paragraphs, headings, and regular text.
  • Screen readers have their own navigation model entirely separate from keyboard-only navigation
  • VoiceOver uses Control+Option+Arrow to navigate, not just arrow keys
  • There are different modes: reading continuously, navigating by headings, navigating by landmarks, etc.

VoiceOver basics I wish I'd known:

  • Command+F5 to turn it on/off
  • Control+Option+Right Arrow to read next item
  • Control+Option+A to start reading continuously from current position
  • Control+Option+Command+H to jump between headings
  • Control+Option+U to open the rotor (navigation menu)

Why this matters for developers:

If you're building accessible sites but never actually use a screen reader, you're guessing. The semantic HTML and ARIA attributes matter, but you won't know if they're working until you hear them announced.

I initially over-complicated things with redundant ARIA labels because I didn't understand how screen readers actually navigate. After using VoiceOver for 10 minutes, the problems became obvious.

Recommendation: Install NVDA (free, Windows) or use VoiceOver (built into macOS) and actually navigate your site blindfolded. It's humbling and educational.

Build & Deploy

All changes built successfully with Next.js:

npm run build

No errors, no warnings. Ready to deploy.

Fix #9: Screen Reader Experience Improvements

Problem: Real-world testing with VoiceOver revealed several issues:

  • The About section was being skipped because it used a visually hidden heading
  • Blog post titles were announced twice (once from link, once from aria-label)
  • Blog post descriptions and dates were being skipped
  • Individual blog posts had poor contrast and missing semantic elements

Solution:

Homepage:

  • Changed About section from hidden H2 to visible H2 with proper styling
  • Removed redundant aria-label="About Natalie" in favor of aria-labelledby
  • Now screen readers navigate naturally: Name → About → Links → Projects → Contact

Blog List Page:

  • Removed redundant aria-label from post links (was repeating the title)
  • Removed aria-labelledby from articles (was causing title to be read twice)
  • Simplified structure: let semantic HTML do the work
  • Screen readers now read: Title → Date → Description for each post

Blog Post Page:

  • Improved color contrast: #333#1a1a1a, #666#555, #999#555
  • Wrapped navigation in <nav aria-label="Breadcrumb"> and <nav aria-label="Footer navigation">
  • Used proper <time dateTime> element for publish date
  • Added aria-label to all navigation links for context
  • Better border contrast: #eee#ddd
  • Added screen reader-only H1 inside article for proper document structure

Key Lesson: Over-engineering ARIA attributes can make accessibility worse. Simple, semantic HTML is often the best solution. Real-world testing with actual screen readers reveals issues automated tools miss.

Summary of Changes

Files Modified:

  1. src/app/layout.tsx - Root layout with semantic structure, skip link styles, sr-only class
  2. src/app/skip-to-content.tsx - Client Component for interactive skip link
  3. src/app/page.tsx - Improved contrast, visible About heading, simplified ARIA
  4. src/app/blog/page.tsx - Blog list with proper time elements, removed redundant ARIA
  5. src/app/blog/[slug]/page.tsx - Individual post contrast, nav elements, time element

Accessibility Improvements:

  • ✅ Fixed missing <h1> detection with proper <main> wrapper
  • ✅ Added working skip-to-content link for keyboard users (fixed browser close bug)
  • ✅ Improved color contrast from #666/#999 to #555/#1a1a1a (WCAG AA compliant)
  • ✅ Added ARIA labels only where needed (removed redundant ones after VoiceOver testing)
  • ✅ Used semantic <time> elements with dateTime attributes throughout
  • ✅ Made About heading visible (better UX than visually hidden)
  • ✅ Created clear focus indicators (2px blue outline) for all interactive elements
  • ✅ Fixed nested <main> elements (changed to <section>)
  • ✅ Added proper <nav> elements with aria-labels for navigation regions
  • ✅ Tested with VoiceOver on macOS and fixed all navigation issues

Before/After:

  • Readability: 20 passed, 25 failed → All contrast issues resolved
  • Titles: 0 passed, 1 failed → H1 detection working
  • Focus visibility: Inconsistent → Clear 2px blue outline
  • Screen reader experience: Sections skipped, titles repeated → Natural navigation with proper semantics
  • VoiceOver navigation: Broken → Fully functional with Tab key and heading navigation

Key Takeaways

  • Semantic HTML matters. Accessibility checkers look for proper document structure, not just the presence of tags.
  • ARIA labels help, but don't replace good HTML. Use semantic elements first, ARIA as enhancement.
  • Keyboard navigation is non-negotiable. If you can't navigate your site with just the keyboard, it's not accessible.
  • Automated audits catch obvious issues, but manual testing is still essential. Screen readers and real users will find things tools can't.
  • Next.js App Router requires thoughtful component splitting. Server Components can't have event handlers, so interactive elements need to be extracted to Client Components.
  • Color contrast is subtle but critical. The difference between #666 and #555 might seem small visually, but it can make or break accessibility compliance.
  • You can't test accessibility without using assistive technology. Automated audits catch structural issues, but only real screen reader testing reveals UX problems. The learning curve is steeper than expected—don't assume Tab navigation is enough.

Next Steps

  • Address remaining color contrast issues
  • Test with actual screen reader users
  • Add reduced motion preferences for animations
  • Consider adding a dark mode with WCAG-compliant contrast ratios

Accessibility isn't a checkbox. It's an ongoing process.


Have feedback on these accessibility improvements? Ping me on Twitter @nataliefloraa or open an issue on GitHub.