January 10, 2026
16 min
Most agencies treat accessibility like a final coat of paint. Here's why that approach fails—and how to architect inclusion from the first line of code.

Pio Greeff
Founder & Lead Developer
Deep dive article
Most agencies treat accessibility like a final coat of paint. Here's why that approach fails—and how to architect inclusion from the first line of code.
The European Accessibility Act enforcement begins June 28, 2025. By now, you've either planned for it or you're about to have a very expensive problem.
But here's what frustrates me about the accessibility conversation: we're still treating it as a checkbox exercise. An audit. A remediation project. Something you bolt onto a finished product and hope passes muster.
This approach is fundamentally broken.
Accessibility isn't a feature. It's an architectural decision. And like all architectural decisions, the cost of getting it wrong compounds exponentially the longer you wait to address it.
I've watched agencies quote €50,000 remediation projects for sites that would have cost €5,000 extra to build correctly from day one. I've seen "accessible" redesigns that technically pass automated tests while remaining functionally unusable for the people they claim to serve.
This guide is different. We're going to talk about accessibility the way structural engineers talk about load-bearing walls—as a foundational constraint that shapes every decision that follows.
Let me give you some numbers that rarely appear in agency proposals.
A 2023 study by the WebAIM Million analyzed the homepages of the top one million websites. 96.3% had detectable WCAG failures. The average page had 50 distinct accessibility errors.
Now consider what "fixing" actually means in practice.
When accessibility is an afterthought, remediation typically requires touching every template, every component, every content entry. You're not fixing bugs—you're retrofitting an architecture that was never designed to support the load.
Discovery phase: Automated scans flag hundreds of issues. Manual testing reveals structural problems the scanners missed. The component library has no semantic foundation. The CMS was configured without accessibility fields. Content editors have been writing image alt text like "IMG_2847.jpg" for three years.
Remediation phase: Developers rebuild components. Designers revise color palettes. Content teams manually update thousands of entries. QA cycles extend because every fix potentially breaks something else.
Maintenance phase: There is no maintenance phase. Six months later, new content has reintroduced the same problems because the underlying systems don't enforce accessibility. You're back to square one.
Design phase: Color contrast ratios are specified in the design system. Interactive states are defined for keyboard and screen reader users. Typography scales are tested for readability.
Development phase: Components are built with semantic HTML foundations. ARIA patterns are standardized and documented. Automated accessibility testing runs in CI/CD. Developers can't merge code that fails baseline checks.
Content phase: The CMS requires alt text before images publish. Heading hierarchies are enforced by templates. Content guidelines include accessibility requirements.
Maintenance phase: The system maintains itself. New content inherits accessible patterns. New developers learn accessible practices because that's how the codebase works.
The retrofit approach costs 10x more and delivers 10x less. But it's how 90% of the industry operates because accessibility is framed as a problem to solve rather than a principle to embody.
The Web Content Accessibility Guidelines 2.2 became a W3C Recommendation in October 2023. If you're still referencing WCAG 2.1 in your compliance documentation, you're already behind.
Here's what matters for architects and developers:
This is the one that's going to hurt the most retrofits.
When a component receives keyboard focus, the focus indicator must meet specific size and contrast requirements. WCAG 2.2 specifies a minimum focus indicator area of at least a 2 CSS pixel thick perimeter around the component, or an area at least as large as a 1 CSS pixel thick perimeter with a 4:1 contrast ratio against the unfocused state.
Translation: those barely-visible focus rings your designer specified because "the blue outline looks ugly"? Non-compliant.
Architectural implication: Your design system needs a standardized, tested focus state for every interactive element. This isn't a CSS afterthought—it's a foundational design token that affects buttons, links, form fields, cards, accordions, tabs, and every custom component your team builds.
Any functionality that uses dragging must have a single-pointer alternative. Drag-and-drop interfaces, sliders, map interactions, sortable lists—all need non-dragging alternatives.
This seems straightforward until you audit your actual codebase. That beautiful Kanban board? The image carousel with swipe gestures? The custom range slider? All need alternatives.
Architectural implication: Component specifications must include pointer-agnostic interaction patterns from the start. Every component that accepts drag input needs a documented alternative interaction method.
Interactive targets must be at least 24×24 CSS pixels, with some exceptions for inline links and legally required elements.
Most design systems already exceed this for buttons, but the failures appear in icon buttons, close buttons, pagination dots, and inline controls. That 16px hamburger icon? Non-compliant. The tiny "x" to dismiss a toast notification? Non-compliant.
Architectural implication: Your design system's spacing and sizing scales must establish 24px as the minimum interactive target size.
If your site provides help mechanisms (contact information, human contact options, self-help options, or a fully automated contact mechanism), these must appear in the same relative location across pages.
Architectural implication: Help patterns should be defined at the layout template level, not implemented ad-hoc per page.
Information previously entered by or provided to the user that is required on subsequent steps must either be auto-populated or available for user selection.
Multi-step forms that ask for the same information repeatedly? Non-compliant. Checkout flows that make you re-enter your shipping address as billing address? Non-compliant.
Architectural implication: Form architectures need data persistence and pre-population strategies designed from the start.
Every accessibility conversation should start with semantic HTML. It's the most powerful accessibility tool we have, and it's free.
Screen readers navigate by document landmarks. If your HTML doesn't define these landmarks, users can't navigate your page efficiently.
This structure is non-negotiable. It's not a "nice to have"—it's the architectural skeleton that makes everything else work.
Headings aren't typography choices. They're document structure.
A proper heading hierarchy lets screen reader users understand your content architecture and navigate between sections. When designers specify heading levels based on visual size rather than document structure, they break this fundamental navigation pattern.
The architectural rule: Every page has exactly one <h1>. Subsequent headings follow a logical hierarchy without skipping levels. If you need a visually smaller heading at a structurally higher level, use CSS—don't break the document outline.
This is where most "accessible" sites fail automated and manual tests alike.
The rule is simple: use the right element for the job.
<a href>.<button>.<button type="submit"> or <input type="submit">.When you use a <div> with an onClick handler instead of a <button>, you lose:
disabled attribute natively)Architectural implication: Component libraries must enforce semantic elements. If a component renders a clickable element, it must use <button> or <a> at its root—never a styled <div> or <span>.
Let's get specific about how accessibility shapes component design.
Every component in an accessible design system should fulfill this contract:
Here's what this looks like in practice for a common pattern—the disclosure widget:
Note what's happening here:
<button> provides keyboard accessibility and semantic meaningaria-expanded communicates the current statearia-controls creates a programmatic relationship between trigger and contenthidden attribute removes the content from the accessibility tree when collapsedaria-hidden because it's decorative—the button text provides the meaningThis is what "accessibility as architecture" means. The component can't be used incorrectly.
Forms are where accessibility implementations most commonly fail, because forms require coordination between multiple elements.
This pattern ensures:
aria-describedbyrole="alert" for immediate announcementaria-invalidIf accessibility testing happens only during audits, you've already failed. Accessibility must be verified continuously, automatically, as part of your development pipeline.
Level 1: Static analysis (fastest, always run)
ESLint plugins like eslint-plugin-jsx-a11y catch issues at write time. These aren't optional—they should be required for merge.
Level 2: Component testing (fast, run on commit)
Test individual components for accessibility using tools like jest-axe or Testing Library's accessibility assertions.
Level 3: Integration testing (medium speed, run in CI)
Run axe-core against rendered pages in your testing environment.
Level 4: Manual testing (slowest, run periodically)
Automated tests catch roughly 30-50% of accessibility issues. Manual testing—keyboard navigation, screen reader testing, zoom testing—catches the rest. Schedule this quarterly at minimum.
Automated accessibility testing must block deployment. Not warn. Block. Treat accessibility failures the same as failing unit tests or security vulnerabilities.
A design system without accessibility built in will produce inaccessible products.
Your color palette should be defined with contrast ratios in mind:
Every color pairing in your system should be documented with its contrast ratio and acceptable use cases.
Your type scale must remain readable at 200% zoom without horizontal scrolling or content loss.
Respect user preferences for reduced motion:
Let's talk dates. (For a broader view of how these fit into the global regulatory landscape, see our piece on dynamic policy management.)
| Regulation | Deadline | Standard |
|---|---|---|
| European Accessibility Act (EAA) | June 28, 2025 | EN 301 549 (aligned with WCAG 2.2 AA) |
| ADA (United States) | Ongoing | Courts reference WCAG; 4,000+ lawsuits annually |
| AODA (Ontario, Canada) | Since 2021 | WCAG 2.0 Level AA for 50+ employees |
The regulatory direction is clear: WCAG 2.2 AA is becoming the global baseline. Building to a lower standard is building technical debt.
Here's how to transition from retrofit accessibility to architectural accessibility:
| Phase | Timeline | Focus |
|---|---|---|
| Audit and Baseline | Weeks 1-4 | Automated scans, manual testing, document issues |
| Design System Remediation | Weeks 5-12 | Update tokens, rebuild components, implement testing |
| Template Updates | Weeks 13-16 | Landmarks, skip links, heading hierarchies |
| Content Remediation | Weeks 17-24 | Alt text, link text, form labels, captions |
| Process Integration | Ongoing | CI/CD testing, quarterly audits, team training |
Legal risk reduction: Accessibility lawsuits are increasing year over year. Settlement costs average $10,000-$100,000 for small businesses, significantly more for enterprises.
Market expansion: 15-20% of the global population has some form of disability. An inaccessible website excludes potential customers.
SEO benefits: Many accessibility practices (semantic HTML, proper heading structure, image alt text) directly improve search engine optimization.
Performance correlation: Accessible sites tend to be faster and more performant. For a deep dive into performance metrics, see our Core Web Vitals guide.
But fundamentally, the business case shouldn't be necessary. Building websites that exclude people based on disability is wrong. It should be as unthinkable as building a physical store with stairs-only access.
Accessibility isn't a feature to add. It's a constraint to embrace.
Like responsive design before it, accessibility is becoming a non-negotiable baseline rather than a competitive advantage. The question isn't whether to implement it, but whether to implement it correctly from the start or expensively retrofit it later.
The architectural approach costs less, delivers more, and maintains itself over time.
If you're starting a new project, build accessibility into your architecture from day one. If you're maintaining an existing project, plan a systematic transition from retrofit to architectural accessibility.
Foundational Learning
Deep Dives
Related Articles
Found this useful?
Share it with your network
/* Architectural approach: define focus as a system-wide token */:root { --focus-ring-width: 3px; --focus-ring-color: #0066cc; --focus-ring-offset: 2px;} *:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: var(--focus-ring-offset);}<!-- Architectural HTML structure --><body> <header role="banner"> <nav aria-label="Main navigation">...</nav> </header> <main id="main-content"> <article> <h1>Page Title</h1> <!-- Content with proper heading hierarchy --> </article> <aside aria-label="Related content">...</aside> </main> <footer role="contentinfo">...</footer></body>/* Typography and structure are separate concerns */.visually-h2 { /* Styles that look like an h3 but remain an h2 structurally */ font-size: var(--font-size-md);}// Accessible disclosure componentfunction Disclosure({ title, children, defaultOpen = false }) { const [isOpen, setIsOpen] = useState(defaultOpen); const contentId = useId(); return ( <div className="disclosure"> <button type="button" aria-expanded={isOpen} aria-controls={contentId} onClick={() => setIsOpen(!isOpen)} className="disclosure-trigger" > <span>{title}</span> <ChevronIcon aria-hidden="true" className={isOpen ? 'rotated' : ''} /> </button> <div id={contentId} role="region" hidden={!isOpen} className="disclosure-content" > {children} </div> </div> );}// Accessible form field patternfunction TextField({ label, error, description, required, ...inputProps }) { const inputId = useId(); const descriptionId = useId(); const errorId = useId(); const describedBy = [ description && descriptionId, error && errorId, ].filter(Boolean).join(' ') || undefined; return ( <div className="field"> <label htmlFor={inputId}> {label} {required && <span aria-hidden="true"> *</span>} {required && <span className="sr-only"> (required)</span>} </label> {description && ( <p id={descriptionId} className="field-description"> {description} </p> )} <input id={inputId} aria-describedby={describedBy} aria-invalid={error ? 'true' : undefined} aria-required={required} {...inputProps} /> {error && ( <p id={errorId} role="alert" className="field-error"> {error} </p> )} </div> );}{ "extends": ["plugin:jsx-a11y/recommended"], "rules": { "jsx-a11y/alt-text": "error", "jsx-a11y/anchor-has-content": "error", "jsx-a11y/click-events-have-key-events": "error", "jsx-a11y/no-static-element-interactions": "error" }}import { axe, toHaveNoViolations } from 'jest-axe';import { render } from '@testing-library/react'; expect.extend(toHaveNoViolations); test('Button has no accessibility violations', async () => { const { container } = render(<Button>Click me</Button>); const results = await axe(container); expect(results).toHaveNoViolations();});// Playwright exampleimport { test, expect } from '@playwright/test';import AxeBuilder from '@axe-core/playwright'; test('homepage has no critical accessibility issues', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa', 'wcag22aa']) .analyze(); expect(results.violations).toEqual([]);});// Design tokens with accessibility built inconst colors = { text: { primary: '#1a1a1a', // 16.1:1 on white secondary: '#595959', // 7.1:1 on white muted: '#767676', // 4.54:1 on white (AA minimum) }, interactive: { primary: '#0055cc', // 7.3:1 on white hover: '#003d99', // 9.8:1 on white }};/* Use relative units for typography */:root { --font-size-sm: 0.875rem; --font-size-base: 1rem; --font-size-lg: 1.25rem; --font-size-xl: 1.5rem;} /* Line heights should be unitless */body { line-height: 1.5;}.animated-element { transition: transform 0.3s ease;} @media (prefers-reduced-motion: reduce) { .animated-element { transition: none; }}