Accessibility is not an optional feature — it's a fundamental right. It ensures that everyone, regardless of their abilities, can use your website or service.
In the US, accessibility is governed by the ADA, and non-compliance has already led to high-profile lawsuits. In the EU and France, it's enforced by the RGAA, with oversight from bodies like the Défenseur des Droits and Arcom (formerly Hadopi). Since 2022, the French government can issue financial penalties — up to €25,000 per year of non-compliance.
Public institutions, large private companies, and e-commerce businesses are the first targets — but everyone should care. Not just to avoid fines, but to offer a better, more inclusive experience.
Part 1 – Accessible Design
Accessibility starts with the design phase. If your Figma file excludes users, your code will too. Here's what to think about when designing:
- Color contrast: Use tools like UseContrast or the Stark Figma plugin to check WCAG-compliant contrasts.
- Text hierarchy: Define consistent, logical heading styles (H1–H6 equivalents) and avoid skipping levels.
- Interactive target size: Ensure all buttons and clickable elements are at least 44×44px.
- Focus styles: Make keyboard focus visible. Use a consistent color or border style in your components.
- Navigation: Think linear. The reading order must make sense even without visuals — test with plugins like SimulAT.
- Modal & overlay logic: Plan where focus lands, how it’s trapped, and where it returns when the modal closes.
- Don't rely on color alone: Use text labels or icons with distinct shapes.
- Dark mode & high contrast: Design with sufficient contrast in all themes.
Part 2 – Accessible Development
Developers are the gatekeepers of actual accessibility. Here are technical foundations and patterns to follow:
- Semantic HTML: No div soup. Use structural tags.
<main>
<article>
<h2>Blog Post</h2>
<p>Content goes here.</p>
</article>
</main>
- Navigation menu:
<nav aria-label="Main navigation">
<ul>
<li><a href="/about">About</a></li>
<li><a href="/services">Services</a></li>
</ul>
</nav>
- Breadcrumbs:
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/section">Section</a></li>
<li aria-current="page">Current Page</li>
</ol>
</nav>
- Labels & descriptions:
<input id="email" aria-labelledby="label1" aria-describedby="email-help" />
<label id="label1" for="email">Email address</label>
<div id="email-help">We never share your email.</div>
- Modal accessibility:
// Button trigger
<button id="openModal" aria-haspopup="dialog" aria-controls="modal1">
Open modal
</button>
// Modal structure
<div class="modal" id="modal1" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDesc" hidden>
<div class="modal-content" role="document">
<h2 id="modalTitle">Subscribe to our newsletter</h2>
<p id="modalDesc">Stay in the loop — once a month max.</p>
<button id="closeModal" aria-label="Close modal">✕</button>
</div>
</div>
// JS with basic focus trap
const openModalBtn = document.getElementById('openModal');
const closeModalBtn = document.getElementById('closeModal');
const modal = document.getElementById('modal1');
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
let firstFocusable, lastFocusable;
openModalBtn.addEventListener('click', () => {
modal.hidden = false;
const focusables = modal.querySelectorAll(focusableElements);
firstFocusable = focusables[0];
lastFocusable = focusables[focusables.length - 1];
firstFocusable.focus();
document.addEventListener('keydown', trapFocus);
});
closeModalBtn.addEventListener('click', closeModal);
function closeModal() {
modal.hidden = true;
openModalBtn.focus();
document.removeEventListener('keydown', trapFocus);
}
function trapFocus(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
- Error alerts:
<div role="alert" aria-live="assertive" class="error-message">
Please enter a valid email.
</div>
- Tabindex for control:
<div tabindex="0" role="button" aria-pressed="false">Custom toggle</div>
- Zoom 400%:
/* Start mobile-first.
Consider 400% zoom as roughly equivalent to a small viewport.
If it's accessible and usable on small screens, it will stay accessible at high zoom levels.
Always avoid absolute px units on text to let browsers scale properly. */
body {
font-size: 1rem;
line-height: 1.5;
padding: 1rem;
}
img {
max-width: 100%;
height: auto;
}
/* Then progressively enhance for larger screens */
@media (min-width: 600px) {
body {
padding: 2rem;
font-size: 1.125rem;
}
}
@media (min-width: 1024px) {
body {
padding: 3rem;
font-size: 1.25rem;
}
}