Building Accessible Next.js Apps: A Real Case Study
Explore how we built an accessible Next.js application, tackling WCAG compliance, keyboard navigation, and screen reader optimization with practical solutions.
By Sean WeldonBuilding Accessible Next.js Apps: A Real Case Study
I recently rebuilt a client's booking platform, and accessibility wasn't a checkbox item. It was the foundation. The client ran a healthcare practice serving patients with various disabilities, so WCAG 2.1 AA compliance wasn't optional. This case study walks through how we built a Next.js app that actually works for everyone.
Why Accessibility Matters for Every Next.js Developer
Most developers I talk to treat accessibility as a post-launch audit item. That's backwards. When you're a next.js developer building production applications, accessibility should inform your component architecture from day one. It's not about adding ARIA labels at the end. It's about structuring your app so assistive technologies can parse it correctly.
Our client needed an appointment booking system. Users had to navigate multi-step forms, view availability calendars, and manage appointments. All of this had to work flawlessly with screen readers, keyboard navigation, and high-contrast modes.
The Technical Foundation
We started with Next.js 14 and the App Router. Server Components gave us an advantage: less JavaScript shipped to the client meant faster initial page loads, which directly impacts users on assistive devices or slower connections.
Here's what mattered most:
Semantic HTML over divs. Every interactive element used the correct native element. Buttons were <button>, links were <a>, form fields used proper <input> types. This sounds basic, but you'd be shocked how many React apps I've audited that use <div onClick> for everything.
Focus management. When users submitted a form step, we moved focus to the next section heading using useRef and focus(). When modals opened, focus trapped inside. When they closed, focus returned to the trigger button.
Color contrast. We used Tailwind's default palette but tested every combination with tools like WebAIM's contrast checker. Text/background ratios hit at least 4.5:1 for normal text, 3:1 for large text.
Real Implementation: The Booking Calendar
The calendar component was the hardest piece. We needed a visual grid that also worked as a keyboard-navigable table for screen reader users.
export function BookingCalendar({ availableSlots }: Props) {
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const gridRef = useRef<HTMLDivElement>(null);
const handleKeyDown = (e: KeyboardEvent, date: Date) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setSelectedDate(date);
}
};
return (
<div role="grid" aria-label="Available appointment dates" ref={gridRef}>
{/* Grid implementation */}
</div>
);
}
Each date cell was a button with proper ARIA attributes. We used role="grid" and role="gridcell" to communicate the structure to screen readers. Arrow key navigation let users move through dates without a mouse.
For the time slot picker, we used a native <select> instead of building a custom dropdown. Native elements handle accessibility automatically. Custom components require dozens of ARIA attributes and keyboard handlers to replicate what the browser gives you for free.
Form Validation and Error Handling
Error messages were the most critical accessibility piece. When a user submitted invalid data, three things had to happen:
- An error summary appeared at the top of the form with focus moved to it
- Each invalid field got an
aria-describedbypointing to its error message - The error messages used
role="alert"so screen readers announced them immediately
{errors.email && (
<p
id="email-error"
role="alert"
className="text-red-600 text-sm mt-1"
>
{errors.email}
</p>
)}
<input
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : undefined}
/>
This pattern worked across every form in the app. Users never had to hunt for what went wrong.
Testing with Real Tools
We didn't rely on automated accessibility checkers alone. I used NVDA and VoiceOver to navigate the entire booking flow myself. Automated tools catch maybe 30% of accessibility issues. The rest require human testing.
Here's what we found during manual testing:
- The loading spinner needed
aria-live="polite"so screen readers announced when data was loading - Skip links needed higher z-index values to appear above the header when focused
- Modal close buttons needed visible focus indicators beyond the default browser outline
Every next.js developer should spend time using their app with assistive technology. It's humbling. You realize how many assumptions you make about how people interact with interfaces.
Performance Impacts of Accessibility
Making the app accessible also made it faster. Semantic HTML meant less CSS to override browser defaults. Native form elements required less JavaScript for interaction handlers. Server Components reduced client-side bundle size.
We measured Core Web Vitals throughout development. Final scores:
- LCP: 1.2s
- FID: 8ms
- CLS: 0.02
These metrics matter for accessibility. Slow page loads and layout shifts disproportionately affect users with cognitive disabilities or older devices.
The Business Case
The client's patient booking rate increased 34% after launch compared to their old system. They also avoided a lawsuit. Two months before we started, a patient had filed an ADA complaint about their previous booking platform.
Accessibility isn't altruism. It's risk management and market expansion. The CDC estimates 26% of U.S. adults live with some form of disability. That's a quarter of your potential users.
What I'd Do Differently
Next time, I'd implement skip navigation earlier in the build. We added it late, which meant refactoring header structure. I'd also use a dedicated focus management library like focus-trap-react instead of rolling our own for modals.
We should have tested with actual users who rely on assistive technology. We did our best with NVDA and VoiceOver, but there's no substitute for real feedback from people who use these tools daily.
Lessons for Your Next Build
Start with semantic HTML. Use native elements whenever possible. Test with a screen reader before you ship. These three practices solve 80% of accessibility problems.
If you're building forms, use proper labels and error messaging patterns. If you're building interactive widgets, learn ARIA roles and keyboard interaction patterns from the WAI-ARIA Authoring Practices.
The techniques in this case study apply whether you're building a booking system or any other next.js application. Accessibility scales across project types.
Work With an Experienced Next.js Developer
Building accessible applications requires planning, testing, and ongoing maintenance. If you're looking for custom web development that prioritizes both accessibility and performance, I help businesses build Next.js applications that work for everyone. Check out my services at sean-weldon.com/webdev.