Skip to main content
Mark Basford

The Three-Layer Component Architecture

Mark Basford50 min

The Three-Layer Component Architecture

Reconciling Accessibility and Aesthetics in Modern UI Design

A Comprehensive White Paper on Eliminating the False Choice Between WCAG Compliance and Visual Excellence


Author: Mark Basford Version: 1.1 Date: March 2026 Classification: Public


Executive Summary

For over a decade, UI engineers have faced an impossible choice: build accessible interfaces that look chunky and outdated, or create beautiful, modern designs that fail users with disabilities. This trade-off has become so normalised that accessibility is often treated as an afterthought—a compliance checkbox rather than a fundamental design constraint.

This white paper introduces the Three-Layer Component Architecture, a design pattern that eliminates this false dichotomy by separating interactive, visual, and content concerns into distinct, purpose-built layers. The pattern ensures WCAG 2.2 AAA compliance while enabling complete visual design freedom.

Key Findings from Our Research

Metric Finding Source
Website failure rate 94.8% of top 1M sites fail accessibility WebAIM Million 2025
Touch error increase 3x higher error rates for motor-impaired users on small targets PMC Research
Conversion impact Good UX increases lead conversion by up to 400% Forrester Research (2009)
Legal risk 2,019 lawsuits filed in H1 2025 (37% increase) EcomBack Report
Widget failures 456 lawsuits targeted sites using accessibility widgets 2025 Mid-Year Report
Framework correlation React sites average 42.4 errors, Vue.js 58.7 errors WebAIM Million

The Three-Layer Architecture provides a path forward: uncompromising accessibility with unrestricted visual design. It's not about choosing between users with disabilities and users who appreciate beautiful design—it's about serving everyone excellently.


Table of Contents

  1. A Decade of Failure: The State of Web Accessibility (2015-2025)
  2. The Human Cost: Understanding Who We're Failing
  3. Why Current Solutions Don't Work
  4. The Business Case: Aesthetics Matter Too
  5. Introducing the Three-Layer Architecture
  6. Implementation Guide: Building a Button Component
  7. Addressing the Gap Challenge
  8. Related Approaches and Prior Art
  9. Limitations and Future Work
  10. Conclusion and Call to Action
  11. References
  12. Glossary

Glossary

Term Definition
ARIA Accessible Rich Internet Applications — a set of HTML attributes that define ways to make web content more accessible to people with disabilities
CVA Class Variance Authority — a utility for managing CSS class variants in component libraries
CSS pixel An abstract unit of measurement used in web design; 1 CSS pixel ≈ 0.26mm at standard viewing distance
dp Density-independent pixel — Android's unit for UI sizing, equivalent to 1 CSS pixel at standard density
EAA European Accessibility Act — EU directive requiring digital products and services to be accessible, enforceable from June 2025
EN 301 549 European standard for ICT accessibility requirements, referenced by the EAA
Fitts' Law A predictive model of human movement that describes the time required to move to a target as a function of distance and target size
Headless UI A component library that provides behaviour and accessibility without visual styling
Layer 1 The accessibility/touch target layer in the Three-Layer Architecture — the invisible interactive element
Layer 2 The visual/presentation layer — provides designed appearance independent of touch target size
Layer 3 The content/state layer — text, icons, state indicators, and visual effects
POUR Perceivable, Operable, Understandable, Robust — the four principles of WCAG
React Aria Adobe's library of React hooks providing accessible UI primitives
Touch target The area of the screen that responds to a user's tap or click; WCAG 2.2 AAA requires a minimum of 44×44 CSS pixels
WCAG Web Content Accessibility Guidelines — the international standard for web accessibility, published by the W3C
WCAG 2.2 AAA The highest conformance level of WCAG 2.2, requiring the most stringent accessibility criteria

Part I: A Decade of Failure: The State of Web Accessibility (2015-2025)

A Lesson From History: The Braille Tragedy

Before examining the current state of web accessibility, we must understand a painful historical parallel—one that demonstrates how the best solution for those who need it most can be ignored for decades when those in power don't prioritise inclusion.

In 1824, a 15-year-old blind French boy named Louis Braille completed a tactile reading system that would revolutionise literacy for the blind. Building on a military "night writing" code, Braille created an elegant system using six raised dots in a cell, capable of representing every letter, number, and musical notation. Users could read quickly with their fingertips and, crucially, write for themselves—something no previous system had allowed.

The system was objectively superior. Blind readers could achieve far greater reading speeds with Braille compared to laboriously tracing raised Roman letters. Blind students who learned Braille dramatically outperformed those using other methods. Helen Keller herself had to learn four different embossed codes just to access printed material, and became a fierce advocate for Braille standardisation.

And yet, full adoption took over a century.

The resistance came not from blind people—who embraced Braille immediately—but from sighted educators and administrators who couldn't use the system themselves. They championed alternatives that they could read, like embossed Roman letters and competing dot systems, despite clear evidence these methods failed blind users.

Timeline of Braille Adoption: "The War of the Dots"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1824        1854        1868        1878        1916        1932
  │           │           │           │           │           │
  ▼           ▼           ▼           ▼           ▼           ▼
┌─────┐    ┌─────┐    ┌─────┐    ┌─────┐    ┌─────┐    ┌─────┐
│Louis│    │France│    │New   │   │Int'l │   │US     │  │Standard│
│Brail│    │adopts│    │York  │   │Cong- │   │schools│  │English │
│le's │    │(2 yrs│    │Point │   │ress  │   │adopt  │  │Braille │
│code │    │after │    │intro-│   │adopts│   │Braille│  │agreed  │
│comp-│    │his   │    │duced │   │Brail-│   │       │  │US & UK │
│leted│    │death)│    │(USA) │   │le    │   │       │  │        │
└─────┘    └─────┘    └─────┘    └─────┘    └─────┘    └─────┘
   │                      │                               │
   │                      └───────────────────────────────┘
   │                        64 years of competing systems
   │                            in the United States

108 YEARS from invention to English-speaking world agreement

Louis Braille died in 1852 at age 43, largely unrecognised. France officially adopted his system two years after his death—but only because blind students refused to stop using it. In the United States, the "War of the Dots" raged for 64 years (1868-1932) as sighted administrators promoted competing systems like New York Point and American Braille. Blind students were caught in the middle, forced to learn multiple incompatible systems depending on which school they attended.

The Pattern Repeats

The tragedy of Braille's delayed adoption reveals a pattern that persists today:

Then (Braille, 1824-1932) Now (Web Accessibility, 2015-2025)
Superior system invented WCAG standards established
Immediate adoption by affected users Assistive tech users embrace accessible sites
Resistance from sighted educators Engineers see accessibility as a checkbox
"It works for us" mentality "Our users don't complain"
Competing systems promoted (New York Point) Overlays and widgets promised as solutions
108 years of preventable suffering Millions excluded from digital services

The tools exist. The standards exist. The expertise exists.

Just as Braille was available in 1824 but not standardised in English-speaking countries until 1932, accessible component patterns exist today but remain optional in every major framework. Material Design recommends 48×48dp touch targets but doesn't enforce them. React Aria enables accessibility but doesn't require it. Bootstrap ships with failing contrast ratios by default.

The majority of engineers treat accessibility as a compliance exercise—a box to tick, an audit to pass, a lawsuit to avoid. It's deprioritised because those making the decisions don't experience the barriers themselves. The "War of the Dots" was fought by sighted administrators who couldn't read any of the systems they championed. Today's accessibility decisions are made by engineers who don't use screen readers.

This white paper proposes a different approach: accessibility that cannot be turned off. Components where the invisible accessibility layer (Layer 1) is structural—not optional. Where choosing our library means choosing WCAG AAA compliance by default.

Louis Braille gave blind people the ability to read and write independently. Over a century of institutional resistance couldn't change the fact that his system was simply better for those who needed it.

Today, 94.8% of websites fail basic accessibility standards. The tools are better than ever. The pattern recognition is clear.

How long will we wait this time?


The Birth of Modern Standards

The journey to our current accessibility crisis began with good intentions. The Web Content Accessibility Guidelines (WCAG) evolved through several major versions, each attempting to address the growing complexity of web applications:

Timeline of WCAG Development
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1999        2008        2018        2023                    Future
  │           │           │           │                       │
  ▼           ▼           ▼           ▼                       ▼
┌─────┐    ┌─────┐    ┌─────┐    ┌─────┐                 ┌─────┐
│WCAG │    │WCAG │    │WCAG │    │WCAG │                 │WCAG │
│ 1.0 │    │ 2.0 │    │ 2.1 │    │ 2.2 │                 │ 3.0 │
└─────┘    └─────┘    └─────┘    └─────┘                 └─────┘
   │           │           │           │                       │
   │           │           │           │                       │
HTML-      Technology  Mobile &   Focus                   Complete
focused    agnostic    cognitive  indicators               overhaul
14 guide-  61 success  +17 new    +9 new                  (~2028)
lines      criteria    criteria   criteria

WCAG 1.0 (1999): Focused primarily on HTML, with 14 guidelines and the A/AA/AAA conformance levels we still use today. This was a web of static pages—JavaScript was primitive, CSS barely existed, and mobile phones couldn't browse the internet.

WCAG 2.0 (2008): A decade-long effort produced technology-agnostic guidelines. The four principles—Perceivable, Operable, Understandable, Robust (POUR)—became the foundation. But by 2008, the iPhone had already launched, and the mobile revolution was underway.

WCAG 2.1 (2018): Finally addressed mobile and cognitive accessibility with 17 new success criteria. Critically, this version introduced Success Criterion 2.5.5: Target Size (Enhanced) at Level AAA, requiring 44×44 CSS pixel touch targets. But because it was AAA (not AA), most organisations ignored it.

WCAG 2.2 (2023): Added 9 new success criteria, including 2.5.8: Target Size (Minimum) at Level AA, requiring 24×24 CSS pixels. This was progress, but the damage was done—a decade of mobile-first design had normalised tiny touch targets.

WCAG 3.0 (Working Draft, expected ~2028): A complete overhaul of the conformance model. The familiar A/AA/AAA levels will be replaced by Bronze, Silver, and Gold ratings, with more flexible evaluation methods including rubrics, sliding scales, and user research validation. While still years from completion, WCAG 3.0 signals a shift from binary pass/fail criteria toward a more nuanced understanding of accessibility quality—one that aligns well with architectural approaches like the Three-Layer pattern.

The WebAIM Million: Seven Years of Evidence

Since 2019, WebAIM has conducted annual accessibility analyses of the top one million websites. The results are damning:

Average Accessibility Errors Per Page
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

70 ┤

60 ┤ ■ 59.6  ■ 60.9                                ■ 56.8
   │              ╲                               ╱      ╲
   │               ╲                             ╱        ╲
50 ┤                ■ 51.4  ■ 50.8  ■ 50.0  ━━━╱          ■ 51.0


40 ┤

30 ┤

20 ┤

10 ┤

 0 ┼────────────────────────────────────────────────────────────────────
     2019    2020    2021    2022    2023    2024    2025

Note: 2020 saw a slight INCREASE (60.9) before dramatic improvements in 2021-2023.
2024 saw a significant spike (56.8 errors) before improving in 2025 (51.0).

WCAG Failure Rates

Year Failure Rate Change Errors/Page
2019 97.8% Baseline 59.6
2020 98.1% +0.3% 60.9
2021 97.4% -0.7% 51.4
2022 96.8% -0.6% 50.8
2023 96.3% -0.5% 50.0
2024 95.9% -0.4% 56.8
2025 94.8% -1.1% 51.0

In seven years, the failure rate dropped by only 3 percentage points. At this rate, achieving broad web accessibility would take decades.

Page Complexity Growth

While error rates slowly declined, page complexity exploded:

Home Page Elements (Average)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1400 ┤

1200 ┤                                                   ■ 1,257 (+61% since 2019)
     │                                              ╱
1000 ┤                               ■ 1,050  ■ 1,173
     │                          ╱
 800 ┤        ■ 864  ■ 887  ■ 955
     │   ╱
 600 ┤■ 782

 400 ┤

 200 ┤

   0 ┼────────────────────────────────────────────────────────────────────
       2019   2020   2021   2022   2023   2024   2025

Data: 782 (2019) → 864 (2020) → 887 (2021) → 955 (2022) → 1,050 (2023) → 1,173 (2024) → 1,257 (2025)

The web is getting more complex faster than it's getting more accessible. Users with disabilities encounter an error approximately once every 24 elements they interact with.

The Six Persistent Failures

The same six error categories have dominated every single year since 2019:

Distribution of Accessibility Errors (2025)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Low contrast text          ████████████████████████████████████████ 79.1%
                           (29.6 instances per page)

Missing alt text           █████████████████████████ 55.5%

Missing form labels        ██████████████████████ 48.2%
                           (Getting WORSE year-over-year)

Empty links                ████████████████████ 45.4%

Empty buttons              █████████████ 29.6%

Missing document language  ███████ 15.8%

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
These 6 categories account for 96% of ALL detected errors

Addressing just these six issues would transform web accessibility. Yet year after year, they persist.

The Framework Factor

The relationship between JavaScript frameworks and accessibility errors is nuanced — React sites now average slightly fewer errors than sites with no framework, but Vue.js and UI libraries like Bootstrap and jQuery UI correlate with significantly more:

Average Errors by Framework (2025)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

No framework    ████████████████████████████████████████████ ~45 errors

React           ██████████████████████████████████████████ 42.4 errors

Vue.js          ██████████████████████████████████████████████████████████ 58.7 errors

Bootstrap       +12.9 additional errors when present

jQuery UI       +15.2 additional errors when present

The ARIA Paradox

Ironically, pages trying harder to be accessible often fail more:

  • ARIA usage has increased nearly 5x since 2019 (now 106 ARIA attributes per page on average)
  • Pages with ARIA present average 111% more errors than pages without (57 vs 27 errors)
  • 79.4% of home pages now use ARIA (up from 60.1% in 2019)

This isn't because ARIA is bad—it's because developers are using it incorrectly, without understanding the underlying accessibility principles.

The accessibility lawsuit landscape has intensified dramatically:

2024 Full Year Statistics

Metric Value
ADA Title III federal lawsuits (all) 8,800
Web accessibility-specific lawsuits ~4,187
Year-over-year change +7%
Repeat lawsuits (same company sued again) 41%
Lawsuits against widget users 25%+
Lawsuits since 2018 25,000+

2025 First Half Statistics (H1 2025)

Metric Value
Lawsuits filed (Jan-Jun) 2,019
Year-over-year increase +37%
Projected full-year total ~4,975
E-commerce targets 69% of cases

Geographic Distribution (2025)

ADA Lawsuit Distribution by State
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

New York        ████████████████████████████████ 31.6% (637 lawsuits)

Florida         ████████████████████████ 24.2% (487 lawsuits, +33% YoY)

California      ███████████████████ 18.9% (380 lawsuits)

Illinois        ████████████ 11.8% (237 lawsuits, +746% YoY!)

Other states    ██████████████ 13.5%

Illinois emerged as a new litigation hotspot with a 746% increase, signalling geographic expansion of accessibility litigation.

The Widget Illusion Shattered

In 2024-2025, the accessibility widget industry faced a reckoning:

  • 456 lawsuits in H1 2025 targeted sites using overlay widgets
  • $1 million FTC settlement with accessiBe for misleading compliance claims
  • 22.6% of all lawsuits targeted widget-using sites
  • Widgets failed to provide legal protection in 100% of tested cases

Accessibility cannot be retrofitted with a JavaScript overlay. The architecture must be built right from the start.

The European Accessibility Act (EAA)

While US litigation has driven much of the accessibility conversation, the European Accessibility Act came into force on 28 June 2025, creating the most significant global shift in accessibility regulation to date:

Aspect Detail
Scope All EU member states + non-EU businesses selling to EU customers
Standard EN 301 549, which references WCAG 2.1 AA
Penalties Up to €100,000 or 4% of annual revenue
Coverage Websites, mobile apps, e-commerce, banking, transport, e-books

Unlike the ADA in the United States—where enforcement relies on individual lawsuits—the EAA creates a proactive compliance obligation. Organisations must demonstrate conformance, not merely respond to complaints.

For any business operating internationally, WCAG compliance is no longer a risk-mitigation strategy. It's a legal requirement.


Part II: The Human Cost: Understanding Who We're Failing

Global Disability Statistics

The scale of digital exclusion is staggering:

Global Disability Population (2025)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

World Population: 8 billion
                  ████████████████████████████████████████████████████████

People with disabilities: 1.3 billion (16% - 1 in 6 people)
                          █████████

People with significant functional difficulties: 190-320 million (2-4%)
                                                 ██

One in six people worldwide has a significant disability. This number is growing due to:

  • Increased longevity (age-related disabilities)
  • Better diagnosis of cognitive conditions
  • Rising rates of chronic conditions
  • Improved survival rates from accidents and illness

Motor Impairment: The Touch Target Crisis

Motor impairments directly impact the ability to interact with touchscreens:

Research Findings on Touch Interaction:

Finding Impact
Touch error increase 3x higher pointing errors vs. mouse
Gesture completion time 2x longer (3.4s vs 1.7s)
Gesture consistency 49.7% lower consistency
Minimum recommended target 18mm (updated from older research)

The Fitts' Law Problem

Fitts' Law, a foundational principle of human-computer interaction, states that the time to acquire a target is a function of distance and target size. For touchscreens:

Movement Time = a + b × log₂(2D/W)

Where:
- D = distance to target
- W = width (size) of target
- Smaller W = exponentially longer acquisition time

Research on older adults and motor-impaired users shows:

  • Error rates converge at 12mm target size for both able-bodied and impaired users
  • Below 12mm, impaired users experience disproportionately higher errors
  • Touch input introduces 3D movement complexity not captured by traditional Fitts' models
  • Right-handed users consistently deviate to the right when tapping

The Smartphone Accessibility Gap

Despite smartphones being essential tools, significant gaps remain:

Metric People with Disabilities General Population
Smartphone ownership 72% 85%+
Internet access (household) 59.6% 90%+
Computer ownership 62% 81%

72% of adults with disabilities own smartphones, but many struggle to use them effectively due to:

  • Small touch targets
  • Complex gesture requirements
  • Lack of customisation options
  • Poor contrast and visibility

Mild-to-Moderate Dexterity: The Invisible Majority

Beyond severe motor impairments lies a larger population:

"There is a broad group of technology users with less acute motor challenges where touch input is somewhat usable, but smartphone use still causes pain, frustration, embarrassment, and discrimination due to gaps in accessibility."

This includes:

  • Temporary conditions: Broken arm, surgery recovery, holding a baby
  • Situational impairments: Cold hands, wet fingers, moving vehicle
  • Age-related changes: Natural decline in fine motor control
  • Fatigue effects: End-of-day reduced dexterity

Dexterity needs vary moment to moment, even for the same individual. A touch target that works in the morning may fail in the evening when fatigue sets in.

Cognitive and Neurodivergent Users: The Overlooked Majority

While motor and visual impairments dominate accessibility discussions, an estimated 15-20% of the population identifies as neurodivergent—including dyslexia, ADHD, autism, dyscalculia, and dyspraxia. WCAG 2.2 includes success criteria addressing cognitive accessibility, but implementation remains rare.

For these users, the barriers are different but equally real:

  • Unpredictable interfaces cause anxiety and cognitive overload
  • Dense layouts overwhelm attention and working memory
  • Inconsistent patterns force relearning on every page
  • Poor contrast and typography compound reading difficulties

The Three-Layer Architecture addresses some cognitive concerns implicitly: consistent component patterns reduce cognitive load, clear visual hierarchy aids comprehension, and adequate spacing improves scannability. Layer 3 (content) is where cognitive accessibility matters most—clear labels, predictable state indicators, and meaningful feedback all live in this layer.


Part III: Why Current Solutions Don't Work

The Framework Accessibility Problem

React: Open but Undirected

React's openness is both strength and weakness:

Strengths:

  • Massive accessibility community
  • Libraries like React Aria, Radix UI available
  • Component-based architecture enables consistent patterns

Weaknesses:

  • No out-of-the-box accessibility features
  • Documentation scattered and incomplete
  • Developer responsible for all accessibility decisions
  • Easy to create inaccessible patterns accidentally

Angular: Structured but Underdocumented

Angular's structure should help, but:

Strengths:

  • Enforced project structure
  • Angular Material A11y SDK available
  • Modules enforce some coding standards

Weaknesses:

  • Accessibility documentation "lacking greatly"
  • Not as many third-party accessibility add-ons
  • A11y SDK coverage is incomplete

Vue: Balanced but Underadopted

Vue offers perhaps the best balance:

Strengths:

  • Well-written accessibility documentation
  • Built-in accessibility features
  • Good middle ground between React and Angular

Weaknesses:

  • Smaller ecosystem than React
  • Fewer accessibility-focused libraries
  • Still requires developer discipline

The Real Problem

"JavaScript frameworks like React, Angular and Vue have a very bad reputation when it comes to web accessibility. But is this due to inherent technical limitations or insurmountable problems of those tools?"

The answer is neither. The problem is architectural—frameworks provide building blocks, but don't enforce accessibility patterns. Developers can build anything, including inaccessible interfaces.

Material Design: The Beautiful Failure

Google's Material Design is perhaps the most influential design system of the past decade. Yet its accessibility story is complicated:

Touch Target Guidelines

Material Design recommends:

  • 48×48 dp minimum touch target (Android)
  • Visual elements can be smaller (24×24 dp icons common)
  • Padding should extend touch target beyond visual bounds
Material Design Touch Target Model
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

            ┌─────────────────────────────────────────┐
            │                                         │
            │           48×48 dp touch target         │
            │                                         │
            │         ┌─────────────────┐             │
            │         │                 │             │
            │         │   24×24 dp      │             │
            │         │   visual icon   │             │
            │         │                 │             │
            │         └─────────────────┘             │
            │                                         │
            │                                         │
            └─────────────────────────────────────────┘

"An icon may appear to be 24×24 dp, but the padding surrounding
 it comprises the full 48×48 dp touch target."

In theory, this works. In practice:

The Implementation Gap

  1. Developers misunderstand the model: They see "24×24 icon" and create 24×24 touch targets
  2. Design tools don't enforce it: Figma shows visual bounds, not touch targets
  3. Code doesn't enforce it: CSS doesn't automatically add touch padding
  4. Testing misses it: Visual tests pass, accessibility tests often absent

Material UI (MUI) Accessibility Issues

Community-reported issues include:

  • Missing form labels in Data Grid dropdowns
  • Broken ARIA references in Tabs component
  • Select component labelling failures
  • Focus management inconsistencies

MUI acknowledges: "The overall accessibility of any project built with [MUI] depends in large part on the author's markup, additional styling, and scripting they've included."

This is the fundamental problem: accessibility is the developer's responsibility, not the framework's guarantee.

Bootstrap: Colour Contrast Catastrophe

Bootstrap, used by millions of websites, has a documented accessibility problem:

"Most colors that currently make up Bootstrap's default palette... lead to insufficient color contrast (below the recommended WCAG 2.0 color contrast ratio of 4.5:1) when used against a light background."

The Default Palette Problem

Bootstrap's official documentation admits:

  • Default colours fail WCAG contrast requirements
  • Authors must manually modify colours for compliance
  • Templates advertising "WCAG 2.0 AA Compliant" often aren't

One analysis of a "compliant" Bootstrap template found:

  • 8 WCAG 2.0 AA coding errors
  • 16 alerts
  • 4 contrast errors
  • On the front page alone

The Evolution (and Its Limits)

Bootstrap has improved over versions:

  • Bootstrap 3.x: Significant accessibility issues
  • Bootstrap 4.x: Some improvements, still colour problems
  • Bootstrap 5.x: Better ARIA support, colour issues persist

Bootstrap 5.3 claims: "It should be perfectly possible to create websites and applications with Bootstrap that fulfill WCAG 2.2 (A/AA/AAA)."

Key word: "possible". Not guaranteed. Not default. Possible.

The Headless UI Paradox

Headless UI libraries represent the state of the art in accessibility-aware component design:

What They Get Right

Radix UI:

  • WAI-ARIA compliant components
  • Full keyboard navigation
  • Focus management
  • Screen reader support
  • No styling opinions

Headless UI (Tailwind):

  • Unstyled, accessible primitives
  • React and Vue support
  • Seamless Tailwind integration

React Aria (Adobe):

  • Industry-leading accessibility
  • 30+ language translations
  • 13 calendar systems support
  • Comprehensive ARIA implementation

What They Don't Solve

Despite excellence in behaviour, headless libraries don't solve the touch target problem:

The Headless Library Gap
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Headless libraries provide:
├── ✓ Keyboard navigation
├── ✓ Focus management
├── ✓ ARIA attributes
├── ✓ Screen reader support
└── ✓ Behaviour patterns

Headless libraries DON'T provide:
├── ✗ Touch target sizing
├── ✗ Visual/interactive layer separation
├── ✗ Automatic AAA compliance
└── ✗ Gap/spacing management

The developer still must:
├── Set appropriate sizes
├── Manage touch target vs visual size
├── Ensure contrast ratios
└── Handle high-contrast modes

A headless button is still whatever size the developer makes it. If the developer wants a 20px compact button, they get a 20px touch target—accessibility failure included.

The Accessibility Widget Disaster

The promise was appealing: add a single JavaScript snippet, and your site becomes accessible. Reality proved different.

The Promise vs. Reality

Widget Companies Claimed:

  • "One line of code for ADA compliance"
  • "AI-powered accessibility"
  • "Automatic WCAG conformance"

Reality in 2024-2025:

  • 1,000+ widget-using sites sued in 2024
  • 456 widget-using sites sued in H1 2025
  • $1M FTC settlement for false claims
  • Zero legal protection demonstrated

Why Widgets Fail

Why Accessibility Widgets Cannot Work
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. OVERLAY APPROACH
   ┌────────────────────────────────┐
   │  Original inaccessible site   │ ← Still there
   ├────────────────────────────────┤
   │  Widget JavaScript overlay    │ ← Tries to patch
   └────────────────────────────────┘

   Problems:
   - Can't change underlying HTML structure
   - Can't fix semantic issues
   - Can't add missing alt text accurately
   - Can't resize touch targets reliably

2. AI LIMITATIONS
   - AI can't determine intent
   - Alt text generation often wrong
   - Can't understand business context
   - "A button" isn't helpful

3. USER EXPERIENCE
   - Adds performance overhead
   - Creates inconsistent behaviour
   - Often conflicts with assistive tech
   - Users report worse experience with widgets

You cannot JavaScript your way to accessibility. It must be built into the architecture.


Part IV: The Business Case: Aesthetics Matter Too

Before introducing our solution, we must address a critical concern: Does accessibility mean sacrificing beautiful design?

The research says no—in fact, good design and good accessibility reinforce each other.

The Aesthetic-Usability Effect

In 1995, researchers Masaaki Kurosu and Kaori Kashimura at Hitachi Design Center discovered something remarkable:

"Users are strongly influenced by the aesthetics of any given interface, even when they try to evaluate the underlying functionality of the system."

They tested 26 ATM interface variations with 252 participants and found:

  • Stronger correlation between aesthetic appeal and perceived usability than between aesthetic appeal and actual usability
  • Attractive interfaces were perceived as easier to use, regardless of actual ease
  • First impressions based on aesthetics influenced all subsequent evaluations

The Tolerance Effect

Further research by Tractinsky et al. confirmed:

"Users are more tolerant of minor usability issues when they find an interface visually appealing."

This has profound implications:

  • Beautiful interfaces get better user reviews
  • Users persist longer with attractive designs
  • Minor bugs are forgiven in well-designed products
  • Ugly interfaces are abandoned faster, even if functional

Conversion Rate Impact

The business case for beautiful design is overwhelming:

Design Impact on Conversion Rates
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Good UI design alone:      up to 200% increase in conversions
                           ████████████████████████████████████████

Good UI + Good UX:         up to 400% increase in conversions
                           ████████████████████████████████████████████████████████████████████████████████

Design improvements only:  35% increase in conversions (Baymard Institute)
                           ██████████████

Checkout form redesign:    3.6% uplift = €450,000+/year revenue
(single experiment)        ██

Note: The 200%/400% figures come from Forrester Research's 2009 study "Leaving User Experience To Chance Hurts Companies" by Mike Gualtieri. While the specific percentages date from 2009, subsequent research has consistently validated the strong correlation between UX quality and conversion rates.

Real-World Case Studies

Company Change Result
Walmart Mobile UX improvements 98% increase in mobile orders
Buyakilt.com Added product filters 26% increase in conversions
Hotel booking site UX enhancements 12.5% increase in bookings

First Impression Window

Research from Lindgaard et al. (2006) demonstrates that users form aesthetic judgments about websites in as little as 50 milliseconds—a finding later confirmed by Google's own research. Within that fraction of a second, users form opinions about:

  • Trustworthiness
  • Professionalism
  • Quality
  • Whether to stay or leave

Ugly accessible beats beautiful inaccessible—but beautiful accessible beats both.

The False Dichotomy

The industry has created a false choice:

The Traditional Trade-off (FALSE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

                    Accessibility ◄─────────────────────► Aesthetics
                         │                                     │
                         ▼                                     ▼
                  ┌──────────────┐                     ┌──────────────┐
                  │   44×44px    │                     │   20×20px    │
                  │   "chunky"   │                     │   "sleek"    │
                  │   toggle     │                     │   toggle     │
                  │              │                     │              │
                  │   ✓ WCAG     │                     │   ✗ WCAG     │
                  │   ✗ Modern   │                     │   ✓ Modern   │
                  └──────────────┘                     └──────────────┘

This is a false choice. The Three-Layer Architecture eliminates it.

The Three-Layer Architecture recognises that what users see and what users touch don't have to be the same element.


Part V: Introducing the Three-Layer Architecture

The Inspiration: Robert-Houdin's Mystery Clock

In 1839, French clockmaker Jean-Eugène Robert-Houdin unveiled something that shouldn't have been possible: a clock with transparent glass faces, no visible gears, no apparent mechanism—yet the hands moved precisely, as if by magic.

The secret? A hidden second glass dial positioned behind the visible one, connected by a concealed rod running through the ornate base.

The invisible layer did all the work. The visible layer got all the attention.

This principle—separating what performs the function from what displays the result—is the foundation of the Three-Layer Architecture.

The Core Insight

The thing you touch doesn't have to be the thing you see.

Traditional component design conflates three distinct concerns:

  1. Interaction: Receiving clicks, touches, focus, keyboard events
  2. Appearance: Colours, borders, shadows, animations
  3. Content: Text, icons, state indicators

The Three-Layer Architecture separates these into purpose-built layers:

Three-Layer Component Architecture
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌─────────────────────────────────────────────────────────────────────────┐
│                                                                         │
│                          LAYER 1: TOUCH TARGET                          │
│                                                                         │
│   • Minimum 44×44 CSS pixels (WCAG 2.2 AAA)                            │
│   • Completely transparent (invisible to sighted users)                 │
│   • Receives ALL pointer events (click, touch, hover)                   │
│   • Contains focus ring for keyboard navigation                         │
│   • The actual interactive element (button, input, etc.)                │
│                                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐   │
│   │                                                                 │   │
│   │                    LAYER 2: VISUAL APPEARANCE                   │   │
│   │                                                                 │   │
│   │   • Any size the design requires (20px, 24px, 32px, etc.)      │   │
│   │   • Full theming support (colours, borders, shadows)           │   │
│   │   • Animation and transition effects                            │   │
│   │   • Hover and pressed visual states                             │   │
│   │   • Centred within Layer 1                                      │   │
│   │   • NO pointer events (pointer-events: none on children)        │   │
│   │                                                                 │   │
│   │   ┌─────────────────────────────────────────────────────────┐   │   │
│   │   │                                                         │   │   │
│   │   │              LAYER 3: CONTENT & STATE                   │   │   │
│   │   │                                                         │   │   │
│   │   │   • Text labels, icons, symbols                         │   │   │
│   │   │   • State indicators (checked, loading, disabled)       │   │   │
│   │   │   • Visual effects (ripples, animations)                │   │   │
│   │   │   • Purely presentational                               │   │   │
│   │   │                                                         │   │   │
│   │   └─────────────────────────────────────────────────────────┘   │   │
│   │                                                                 │   │
│   └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Layer-by-Layer Breakdown

Layer 1: The Accessibility Layer (Touch Target)

Purpose: Ensures WCAG 2.2 AAA compliance for all pointer interactions

Characteristics:

  • Always 44×44 CSS pixels minimum (AAA) or 24×24 (AA minimum)
  • Completely transparent—sighted users don't see it
  • Receives all pointer events
  • Contains the focus ring for keyboard navigation
  • Is the actual semantic element (<button>, <input>, etc.)

Critical Design Decision: The focus ring belongs on Layer 1, not Layer 2. This ensures:

  • Focus ring is always 44×44px minimum (WCAG 2.4.13)
  • Focus indicator is visible regardless of visual size
  • Consistent focus behaviour across all visual variants

Layer 2: The Visual Layer (Presentation)

Purpose: Provides the designed appearance without accessibility constraints

Characteristics:

  • Any size the design requires
  • Full theming support
  • Animations and transitions
  • Hover and pressed visual states
  • Centred within Layer 1
  • Typically a <span> or <div> (non-interactive)

Critical Design Decision: Layer 2 has NO focus styles. Focus is handled by Layer 1.

Layer 3: The Content Layer (State Representation)

Purpose: Displays content and communicates state visually

Characteristics:

  • Text labels, icons, symbols
  • State indicators (loading spinners, checkmarks, etc.)
  • Visual effects (ripples, animations)
  • Purely presentational—no interactivity
  • pointer-events: none to prevent event capture

Visual Comparison: Traditional vs. Three-Layer

Traditional Single-Layer Button
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

To meet 44px requirement:

┌──────────────────────────────────────────────┐
│                                              │  44px
│              Save Changes                    │  (looks chunky)
│                                              │
└──────────────────────────────────────────────┘

OR to look modern:

┌─────────────────────────┐
│     Save Changes        │  32px (fails WCAG AAA)
└─────────────────────────┘


Three-Layer Architecture
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌──────────────────────────────────────────────┐
│                                              │
│        ┌─────────────────────────┐           │  44px touch target
│        │     Save Changes        │           │  (invisible)
│        └─────────────────────────┘           │  32px visual
│                                              │  (looks modern)
└──────────────────────────────────────────────┘

Result: Looks like 32px. Touches like 44px. Everyone wins.

Part VI: Implementation Guide: Building a Button Component

Let's build a production-ready Button component using the Three-Layer Architecture. This implementation uses React, TypeScript, React Aria, and Class Variance Authority (CVA).

Technology Stack

Technology Purpose
React Component framework
TypeScript Type safety
React Aria Accessible primitives
CVA (Class Variance Authority) Variant-based styling
Tailwind CSS Utility classes

Step 1: Define Layer 1 (Touch Target)

Layer 1 is the outermost element—the actual interactive button. It handles:

  • Minimum touch target size (44×44px)
  • Focus ring for keyboard navigation
  • Disabled state styling
  • Pointer event handling
/**
 * Layer 1: Transparent outer touch target (44x44px minimum)
 *
 * This layer handles the WCAG 2.2 AAA touch target requirement.
 * It is always transparent and centers the visual button inside.
 *
 * IMPORTANT: Focus ring stays on Layer 1 for AAA compliance (2.4.13)
 */
const buttonOuterVariants = cva(
  // Base styles - always applied
  [
    // Flexbox centering
    "inline-flex items-center justify-center",

    // WCAG 2.2 AAA: 44×44px minimum touch target
    "min-h-[44px] min-w-[44px]",

    // Focus ring on Layer 1 (not Layer 2!)
    "focus-visible:outline-none",
    "focus-visible:ring-2",
    "focus-visible:ring-[var(--ring)]",
    "focus-visible:ring-offset-2",
    "focus-visible:ring-offset-background",

    // Disabled state
    "disabled:pointer-events-none",
    "disabled:opacity-50",
  ].join(" "),
  {
    variants: {
      // Full width variant
      fullWidth: {
        true: "w-full",
        false: "",
      },
    },
    defaultVariants: {
      fullWidth: false,
    },
  }
);

Key Design Decisions for Layer 1

  1. min-h-[44px] min-w-[44px]: Ensures AAA touch target compliance regardless of visual size

  2. Focus ring on Layer 1: The focus indicator wraps the entire 44×44px target, not just the visual element. This ensures keyboard users always see an adequately-sized focus indicator.

  3. No background colour: Layer 1 is transparent—it shouldn't be visible

  4. disabled:pointer-events-none: Prevents interaction when disabled

Step 2: Define Layer 2 (Visual Appearance)

Layer 2 provides the visual appearance. It can be any size, with any styling:

/**
 * Layer 2: Visual button appearance (adjustable size)
 *
 * Provides the visual appearance with configurable size.
 * Can be smaller than touch target for use cases like carousel dots.
 *
 * NOTE: NO focus-visible styles here - focus ring is on Layer 1
 */
const buttonVisualVariants = cva(
  // Base styles
  [
    "inline-flex items-center justify-center gap-2",
    "whitespace-nowrap rounded-md",
    "text-sm font-medium",
    "ring-offset-background transition-colors",

    // SVG icons inside button
    "[&_svg]:pointer-events-none",
    "[&_svg]:size-4",
    "[&_svg]:shrink-0",

    // For ripple effects
    "relative",
    "cursor-pointer",
  ].join(" "),
  {
    variants: {
      // Visual variants (colours, borders)
      variant: {
        default: [
          "bg-[var(--primary-action)]",
          "text-[var(--primary-action-foreground)]",
          "shadow-md",
          "hover:bg-[var(--primary-action-hover)]",
          "data-[pressed]:bg-[var(--primary-action)]/80",
        ].join(" "),

        destructive: [
          "bg-[var(--destructive-background)]",
          "text-[var(--destructive-foreground)]",
          "shadow-md",
          "hover:bg-[var(--destructive-background)]/90",
          "data-[pressed]:bg-[var(--destructive-background)]/80",
        ].join(" "),

        outline: [
          "border border-[var(--input-border)]",
          "bg-[var(--page-background)]",
          "hover:bg-[var(--input-border)]",
          "data-[pressed]:bg-[var(--input-border)]",
        ].join(" "),

        secondary: [
          "bg-[var(--secondary)]",
          "text-[var(--secondary-foreground)]",
          "shadow-md",
          "hover:bg-[var(--secondary)]/80",
          "data-[pressed]:bg-[var(--secondary)]/70",
        ].join(" "),

        ghost: [
          "hover:bg-[var(--accent)]",
          "hover:text-[var(--accent-foreground)]",
          "data-[pressed]:bg-[var(--accent)]",
        ].join(" "),

        link: [
          "text-[var(--text-link)]",
          "underline-offset-4",
          "hover:underline",
          "data-[pressed]:text-[var(--text-link-hover)]",
        ].join(" "),
      },

      // Visual size (independent of touch target!)
      visualSize: {
        default: "h-10 px-4 py-2",        // 40px height
        sm: "h-9 rounded-md px-3 text-xs", // 36px height
        lg: "h-11 rounded-md px-8",        // 44px height
        icon: "h-10 w-10",                 // 40×40px square
        dot: "h-5 w-5 rounded-full p-0 min-h-0 min-w-0", // 20×20px dot
      },

      // Full width
      fullWidth: {
        true: "w-full",
        false: "",
      },
    },
    defaultVariants: {
      variant: "default",
      visualSize: "default",
    },
  }
);

Key Design Decisions for Layer 2

  1. visualSize prop: Explicitly named to indicate it controls ONLY the visual layer, not the touch target

  2. dot size: 20×20px visual inside a 44×44px touch target—perfect for carousel pagination

  3. NO focus styles: Focus ring is on Layer 1. Layer 2 should never have focus-visible:* classes

  4. data-[pressed] instead of :active: React Aria provides isPressed state, which we expose via data attributes for more reliable styling

  5. CSS variables for theming: var(--primary-action) etc. enable runtime theming

Step 3: Define Layer 3 (Content)

Layer 3 elements are nested inside Layer 2:

// Layer 3 elements are typically inline in the JSX
// But for loading spinners, we can define consistent styles:

const loadingSpinnerStyles = [
  "motion-safe:animate-spin",  // WCAG 2.3.3: Respect reduced motion
].join(" ");

const rippleEffectStyles = [
  "absolute inset-0",
  "rounded-[inherit]",
  "bg-current opacity-10",
  "motion-safe:animate-in motion-safe:zoom-in-95",
].join(" ");

Step 4: Compose the Component

"use client";

import { forwardRef, memo, useId, type ReactNode } from 'react';
import {
  Button as AriaButton,
  type ButtonProps as AriaButtonProps,
} from 'react-aria-components';
import { cva, type VariantProps } from 'class-variance-authority';
import { Loader2 } from 'lucide-react';
import { cn } from '../../utils/cn';

// ... CVA definitions from above ...

export interface ButtonProps
  extends Omit<AriaButtonProps, 'className'>,
    VariantProps<typeof buttonVisualVariants> {
  loading?: boolean;
  loadingText?: string;
  shortcut?: string;
  className?: string;
  buttonVisualClassName?: string;
  children?: ReactNode;
  fullWidth?: boolean;

  /**
   * Visual size of the button (Layer 2)
   * The touch target (Layer 1) is ALWAYS 44×44px minimum
   *
   * @example
   * <Button visualSize="dot" /> // 44px touch, 20px visual
   */
  visualSize?: 'sm' | 'default' | 'lg' | 'icon' | 'dot';
}

/**
 * Button Component - 3-Layer Architecture
 *
 * Layer 1: Touch Target (AriaButton) - 44×44px WCAG AAA compliant
 * Layer 2: Visual Button (span) - configurable appearance
 * Layer 3: Content (children) - text, icons, effects
 */
const Button = memo(forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      className,
      buttonVisualClassName,
      variant,
      visualSize = 'default',
      fullWidth,
      loading = false,
      loadingText = "Loading...",
      shortcut,
      children,
      isDisabled,
      ...props
    },
    ref
  ) => {
    // Development warning for accessibility
    if (process.env.NODE_ENV !== 'production') {
      if (
        (visualSize === 'dot' || visualSize === 'icon') &&
        !props['aria-label'] &&
        !children
      ) {
        console.warn(
          '[Button] visualSize="dot" or "icon" requires aria-label ' +
          'when no visible text is provided (WCAG 1.1.1)'
        );
      }
    }

    return (
      // LAYER 1: Touch Target
      <AriaButton
        ref={ref}
        isDisabled={isDisabled || loading}
        className={cn(buttonOuterVariants({ fullWidth }), className)}
        {...props}
      >
        {(renderProps) => (
          // LAYER 2: Visual Button
          <span
            className={cn(
              buttonVisualVariants({
                variant,
                visualSize,
                fullWidth,
              }),
              buttonVisualClassName
            )}
            data-pressed={renderProps.isPressed || undefined}
          >
            {/* LAYER 3: Content & Effects */}

            {/* Loading state */}
            {loading && (
              <>
                <Loader2
                  className="motion-safe:animate-spin"
                  aria-hidden="true"
                />
                <span className="sr-only" aria-live="polite">
                  {loadingText}
                </span>
              </>
            )}

            {/* Normal content */}
            {!loading && children}

            {/* Keyboard shortcut hint (visible on focus) */}
            {renderProps.isFocusVisible && shortcut && (
              <kbd className="ml-auto hidden text-xs opacity-60 lg:inline">
                {shortcut}
              </kbd>
            )}

            {/* Press ripple effect */}
            {renderProps.isPressed && (
              <span
                className="absolute inset-0 rounded-[inherit] bg-current
                           opacity-10 motion-safe:animate-in
                           motion-safe:zoom-in-95"
                aria-hidden="true"
              />
            )}
          </span>
        )}
      </AriaButton>
    );
  }
));

Button.displayName = "Button";

export { Button, buttonOuterVariants, buttonVisualVariants };

Step 5: Usage Examples

Standard Buttons

// Default button - 40px visual, 44px touch
<Button>Save Changes</Button>

// Small button - 36px visual, 44px touch
<Button visualSize="sm">Cancel</Button>

// Large button - 44px visual, 44px touch
<Button visualSize="lg">Submit Application</Button>

Icon Buttons

// Icon button - 40px visual, 44px touch
<Button visualSize="icon" aria-label="Settings">
  <SettingsIcon />
</Button>

// Tiny icon - requires aria-label!
<Button visualSize="icon" aria-label="Close dialog">
  <XIcon className="h-4 w-4" />
</Button>
// Carousel pagination - 20px visual, 44px touch
<div className="flex gap-0">
  {slides.map((_, index) => (
    <Button
      key={index}
      visualSize="dot"
      aria-label={`Go to slide ${index + 1}`}
      variant={currentSlide === index ? "default" : "ghost"}
    />
  ))}
</div>

Loading States

// Loading with screen reader announcement
<Button loading loadingText="Saving your changes...">
  Save
</Button>

Testing the Three-Layer Architecture

Unit Tests

describe('3-Layer Architecture', () => {
  describe('Layer Structure', () => {
    it('renders outer touch target element (Layer 1)', () => {
      render(<Button>Test</Button>);
      const button = screen.getByRole('button');
      expect(button.tagName).toBe('BUTTON');
    });

    it('renders inner visual span element (Layer 2)', () => {
      render(<Button>Test</Button>);
      const button = screen.getByRole('button');
      const visualLayer = button.querySelector('span');
      expect(visualLayer).toBeInTheDocument();
    });

    it('touch target has WCAG-compliant minimum size classes', () => {
      render(<Button>Test</Button>);
      const button = screen.getByRole('button');
      expect(button.className).toContain('min-h-[44px]');
      expect(button.className).toContain('min-w-[44px]');
    });

    it('visual layer receives variant styling', () => {
      render(<Button variant="destructive">Test</Button>);
      const button = screen.getByRole('button');
      const visualLayer = button.querySelector('span');
      expect(visualLayer?.className).toContain('bg-[var(--destructive)]');
    });
  });

  describe('Visual Size Independence', () => {
    it('visualSize="dot" renders small visual inside large touch target', () => {
      render(<Button visualSize="dot" aria-label="Dot">.</Button>);
      const button = screen.getByRole('button');
      const visualLayer = button.querySelector('span');

      // Touch target: 44px minimum
      expect(button.className).toContain('min-h-[44px]');
      expect(button.className).toContain('min-w-[44px]');

      // Visual layer: 20px
      expect(visualLayer?.className).toContain('h-5');
      expect(visualLayer?.className).toContain('w-5');
    });

    it('touch target size unchanged regardless of visualSize', () => {
      const { rerender } = render(
        <Button visualSize="dot" aria-label="Dot" />
      );
      let button = screen.getByRole('button');
      expect(button.className).toContain('min-h-[44px]');

      rerender(<Button visualSize="lg">Large</Button>);
      button = screen.getByRole('button');
      expect(button.className).toContain('min-h-[44px]');
    });
  });

  describe('Focus Ring Location', () => {
    it('focus ring is on Layer 1 (AriaButton), not Layer 2', () => {
      render(<Button>Test</Button>);
      const button = screen.getByRole('button');
      const visualLayer = button.querySelector('span');

      // Focus ring on button
      expect(button.className).toContain('focus-visible:ring-2');
      // NOT on visual layer
      expect(visualLayer?.className).not.toContain('focus-visible:ring');
    });
  });
});

Visual Regression Tests

// Verify visual appearance at different sizes
test('button visual sizes', async ({ page }) => {
  await page.goto('/storybook?id=button--all-sizes');

  // Screenshot each variant
  await expect(page.locator('.button-sm')).toHaveScreenshot('button-sm.png');
  await expect(page.locator('.button-default')).toHaveScreenshot('button-default.png');
  await expect(page.locator('.button-lg')).toHaveScreenshot('button-lg.png');
  await expect(page.locator('.button-dot')).toHaveScreenshot('button-dot.png');
});

// Verify touch target is 44px regardless of visual
test('touch targets meet WCAG AAA', async ({ page }) => {
  await page.goto('/storybook?id=button--dot-size');

  const button = page.locator('button');
  const box = await button.boundingBox();

  expect(box?.width).toBeGreaterThanOrEqual(44);
  expect(box?.height).toBeGreaterThanOrEqual(44);
});

Accessibility Audits

import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

describe('Accessibility', () => {
  const visualSizes = ['sm', 'default', 'lg', 'icon', 'dot'] as const;

  visualSizes.forEach((visualSize) => {
    it(`has no axe violations for visualSize="${visualSize}"`, async () => {
      const { container } = render(
        <Button
          visualSize={visualSize}
          aria-label={`${visualSize} button`}
        >
          {visualSize !== 'dot' && visualSize !== 'icon' ? 'Text' : undefined}
        </Button>
      );
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });
  });
});

Part VII: Addressing the Gap Challenge

The Spacing Problem

The Three-Layer Architecture has one significant trade-off: increased effective spacing between components.

The Gap Challenge Illustrated
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Traditional 32px buttons with 8px gap:

┌────────────────┐ 8px ┌────────────────┐
│     Button     │◄───►│     Button     │
└────────────────┘     └────────────────┘
        32px                  32px

Three-Layer with 32px visual, 44px touch, 8px gap:

┌───────────────────────────┐ 8px ┌───────────────────────────┐
│                           │◄───►│                           │
│   ┌────────────────┐      │     │      ┌────────────────┐   │
│   │     Button     │      │     │      │     Button     │   │
│   └────────────────┘      │     │      └────────────────┘   │
│                           │     │                           │
└───────────────────────────┘     └───────────────────────────┘
          44px                              44px

Visual gap appears to be: 8px + 6px + 6px = 20px
(44-32)/2 = 6px of transparent space on each side

This can make densely-packed UIs feel more spacious than intended.

Anti-Pattern: Overlapping Touch Targets with Z-Index

⚠️ DO NOT USE THIS APPROACH

A tempting but fundamentally flawed "solution" is to allow touch targets to overlap and use z-index to determine which element receives the tap:

// ❌ BAD - DO NOT DO THIS
<div className="flex">
  {/* Touch targets overlap, z-index determines winner */}
  <Button className="z-10">Previous</Button>
  <Button className="z-20">1</Button>
  <Button className="z-30">2</Button>  {/* Steals adjacent touch areas */}
  <Button className="z-20">3</Button>
  <Button className="z-10">Next</Button>
</div>
Why Overlapping Touch Targets FAIL Users
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Visual appearance (what users see):

┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│  <   │ │  1   │ │  2   │ │  3   │ │  >   │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘

What ACTUALLY happens when a motor-impaired user taps:

┌──────────────────────────────────────────────────────────────────────┐
│  User aims │ User aims │ User aims │ User aims │ User aims           │
│  for "<"   │ for "1"   │ for "2"   │ for "3"   │ for ">"             │
│     ↓      │     ↓     │     ↓     │     ↓     │     ↓               │
│  Gets "1"! │ Gets "2"! │ Gets "2"  │ Gets "2"! │ Gets "3"!           │
│  (WRONG)   │ (WRONG)   │ (correct) │ (WRONG)   │ (WRONG)             │
└──────────────────────────────────────────────────────────────────────┘

Higher z-index elements STEAL touches intended for adjacent elements.

Anti-Pattern: Negative Margins

⚠️ DO NOT USE THIS APPROACH

Using negative margins to pull touch targets closer creates the same problem as overlapping with z-index:

/* ❌ BAD - DO NOT DO THIS */
.button-group .button-layer-1 {
  /* Pull adjacent touch targets closer */
  margin-left: -6px;
  margin-right: -6px;
}

Why These Are Harmful

  1. Defeats the purpose: The whole point of 44px touch targets is to give users room for error. Overlapping means taps intended for one element activate another.

  2. Punishes the users we're trying to help: Motor-impaired users who need larger touch targets will experience MORE errors, not fewer.

  3. Creates invisible interaction zones: Users have no way to know where one touch target ends and another begins.

  4. Technical compliance ≠ usability: You might pass an automated audit, but you've made the interface actively worse for users with disabilities.

The gap is the feature, not the problem. If your design can't accommodate proper touch target spacing, redesign—don't compromise accessibility.


Anti-Pattern: Shared Touch Targets

⚠️ DO NOT USE THIS APPROACH

Another tempting "solution" is to wrap multiple controls in a single touch target container:

// ❌ BAD - DO NOT DO THIS
<div
  role="group"
  aria-label="Quantity"
  className="inline-flex min-h-[44px] items-center border rounded-md"
>
  <button
    aria-label="Decrease quantity"
    className="h-8 w-8 flex items-center justify-center"  // Too small!
  >
    -
  </button>
  <span className="w-12 text-center">{quantity}</span>
  <button
    aria-label="Increase quantity"
    className="h-8 w-8 flex items-center justify-center"  // Too small!
  >
    +
  </button>
</div>

Why This Is Harmful

  1. Breaks keyboard navigation: Each interactive control needs its own focusable element. Users can't tab to individual buttons inside a shared target.

  2. Confuses screen readers: The relationship between the group container and child buttons creates ambiguous semantics. Screen readers may announce the group but not properly convey the individual controls.

  3. Individual buttons still have small touch targets: The 44px container doesn't magically make the 32px buttons inside easier to tap. Users still need to hit the small visual button.

  4. Focus ring is on wrong element: The focus indicator appears on the container, not on the actual button being activated.

Each interactive element needs its own 44px touch target. There are no shortcuts.


Anti-Pattern: Responsive Touch Targets

⚠️ DO NOT USE THIS APPROACH

It's tempting to reduce touch target sizes for "precise" input devices:

/* ❌ BAD - DO NOT DO THIS */
/* Base: Mobile-first, large touch targets */
.button-layer-1 {
  min-height: 44px;
  min-width: 44px;
}

/* "Enhancement": Smaller for precise pointers */
@media (pointer: fine) and (hover: hover) {
  .button-layer-1--responsive {
    min-height: 32px;  /* Assumes precision = ability */
    min-width: 32px;
  }
}

Why This Is Harmful

  1. pointer: fine doesn't mean "user has fine motor control": It means the device has a precise pointing mechanism. A mouse is "fine" even when the user has tremors, arthritis, or other motor impairments.

  2. Punishes desktop users with disabilities: Many people with motor impairments use desktop computers with mice. They need large touch targets MORE than mobile users, not less.

  3. False assumptions about input methods:

    • Trackpads detect as pointer: fine but are harder to use precisely
    • Styluses detect as pointer: fine but users may have shaky hands
    • Touch-capable laptops can switch between touch and trackpad mid-session
  4. Violates the principle of progressive enhancement: Accessibility should be the baseline, not something that gets removed based on device capabilities.

The 44px minimum applies to ALL input methods. Device precision ≠ user precision.


Mitigation Strategy 1: Grid-Aware Spacing

Design your grid system to account for touch targets:

/* Grid cells sized to touch target */
.toolbar-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, 44px);
  gap: 0;  /* Touch targets are adjacent */
}

.toolbar-button-visual {
  /* Visual centered in 44px cell */
  width: 32px;
  height: 32px;
  margin: auto;
}
Grid-Aware Layout
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Grid cells (44px each, no gap):

┌────────────┬────────────┬────────────┬────────────┐
│            │            │            │            │
│   ┌────┐   │   ┌────┐   │   ┌────┐   │   ┌────┐   │
│   │Icon│   │   │Icon│   │   │Icon│   │   │Icon│   │
│   └────┘   │   └────┘   │   └────┘   │   └────┘   │
│            │            │            │            │
└────────────┴────────────┴────────────┴────────────┘
    44px         44px         44px         44px

Visuals (32px) appear to have 12px gaps.
Touch targets (44px) are adjacent with no gaps.

When Extra Spacing is Actually Good

In many contexts, the extra spacing is beneficial—and trying to eliminate it is solving a problem that doesn't exist:

Context Why Extra Space Helps
Mobile interfaces Fat-finger-friendly, reduces mis-taps
Primary CTAs Draws attention, creates visual hierarchy
Form controls Breathing room improves scannability
All interfaces Reduces cognitive load and visual clutter
Error-prone actions Prevents accidental destructive clicks

The "gap problem" is often imaginary. Before trying to eliminate spacing:

  1. Question the design requirement: Does the UI actually need to be denser? Compact designs often harm usability for everyone, not just users with disabilities.

  2. Test with real users: Users with motor impairments consistently prefer more spacing, even when it makes the interface look "less modern."

  3. Consider the trade-offs: Every mitigation strategy has costs. Sometimes the cleanest solution is to accept the 44px spacing as a feature.

Scenarios where you might genuinely need denser layouts:

  • Dense data tables (but consider if the table design itself is the problem)
  • Compact toolbars (but could these be redesigned with fewer icons?)
  • Pagination/carousel dots (but use the Three-Layer Architecture—don't shrink touch targets)
  • Icon-only navigation (but should this be icon-only?)

For these specific cases, use the mitigation strategies above.


Part VIII: Related Approaches and Prior Art

React Aria's Architecture

Adobe's React Spectrum project pioneered the separation of concerns in accessible components:

React Aria's Three-Part Architecture
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌─────────────────────────────────────────────────────────────────────────┐
│                                                                         │
│  STATE HOOK            BEHAVIOUR HOOK           RENDERED COMPONENT      │
│  (useButtonState)      (useButton)              (<Button>)              │
│                                                                         │
│  ┌─────────────┐      ┌─────────────────┐      ┌─────────────────────┐  │
│  │ Core logic  │      │ Event handling  │      │ DOM structure       │  │
│  │ Platform-   │ ───► │ Accessibility   │ ───► │ Styling             │  │
│  │ independent │      │ i18n            │      │ Composition         │  │
│  └─────────────┘      └─────────────────┘      └─────────────────────┘  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

How it differs from Three-Layer:

  • React Aria separates behavioural concerns (state, events, accessibility)
  • Three-Layer separates spatial concerns (touch target, visual, content)
  • They're complementary—Three-Layer uses React Aria for Layer 1's behaviour

Headless UI Libraries

Radix UI, Headless UI, and similar libraries separate behaviour from styling:

Headless Library Separation
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌─────────────────────────────────────────────────────────────────────────┐
│                                                                         │
│        HEADLESS LIBRARY                    DEVELOPER STYLING            │
│                                                                         │
│  ┌─────────────────────────────┐      ┌─────────────────────────────┐  │
│  │ Focus management            │      │ Colours                     │  │
│  │ Keyboard navigation         │      │ Borders                     │  │
│  │ ARIA attributes             │      │ Shadows                     │  │
│  │ Screen reader support       │ ───► │ Typography                  │  │
│  │ Portal rendering            │      │ Animations                  │  │
│  │ Click outside detection     │      │ Size (!!!)                  │  │
│  └─────────────────────────────┘      └─────────────────────────────┘  │
│                                                                         │
│  Note: Size is developer's responsibility!                              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

The Gap: Headless libraries don't enforce touch target sizing. Three-Layer fills this gap.

Vue Renderless Components

Vue's renderless component pattern provides behaviour without presentation:

<!-- Renderless toggle providing only behaviour -->
<Toggle v-slot="{ isOn, toggle }">
  <!-- Developer provides all visuals -->
  <button @click="toggle" :class="isOn ? 'bg-green-500' : 'bg-gray-200'">
    {{ isOn ? 'On' : 'Off' }}
  </button>
</Toggle>

Similarity: Separates behaviour from visuals Difference: Doesn't address touch target sizing

Android Jetpack Compose

Compose automatically expands touch targets:

"Even when the visible touch target size of an interactive composable is smaller than Material Design's minimum size, Jetpack Compose will also automatically apply an invisible touch target size region around such a composable so that its touch target size is at least 48dp by 48dp."

This is Three-Layer at the framework level! But:

  • Only available on Android
  • Developers don't have explicit control
  • Can cause unexpected spacing issues

CSS Pseudo-Element Pattern

A common CSS technique uses ::before or ::after for touch expansion:

.small-icon-button {
  position: relative;
  width: 24px;
  height: 24px;
}

.small-icon-button::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 44px;
  height: 44px;
  /* Invisible but clickable */
}

Limitations:

  • Single pseudo-element limits other uses
  • Doesn't work well with focus rings
  • No framework-level guarantees

Comparison Table

Approach Behaviour Styling Touch Target Focus Ring
Material UI Integrated Integrated Padding-based On element
Headless UI Separated Developer Developer Developer
React Aria Separated Developer Developer Developer
Jetpack Compose Framework Developer Framework Framework
CSS Pseudo Manual Integrated Pseudo-element Element
Three-Layer Separated Separated Architectural (Layer 1) Layer 1

Three-Layer is unique in treating touch targets as an architectural layer, not a styling afterthought or framework magic.


Part IX: Limitations and Future Work

No architectural pattern solves everything. The Three-Layer Architecture addresses a specific and critical subset of accessibility concerns—touch target sizing, visual/interactive separation, and focus management—but it is not a complete accessibility solution.

What This Architecture Does Not Address

  • Content quality: Layer 3 provides a place for content, but doesn't guarantee that labels are meaningful, error messages are helpful, or language is plain. Content accessibility remains a human responsibility.
  • Complex interaction patterns: Multi-step workflows, drag-and-drop interfaces, and real-time collaborative editing present accessibility challenges that go beyond component-level architecture.
  • Cognitive load in information-dense interfaces: While consistent component patterns help, the Three-Layer Architecture cannot prevent an overwhelming page layout or a confusing navigation structure. Information architecture is a separate discipline.
  • Dynamic content and live regions: Screen reader announcements for toast notifications, loading states, and real-time updates require careful ARIA live region management that sits outside the three-layer model.
  • Internationalisation edge cases: Right-to-left layouts, variable text length across languages, and culturally-specific interaction patterns may require layer adjustments not covered here.

Future Work

  • Extending to complex components: This paper demonstrates the pattern with a Button. Applying it to date pickers, data tables, comboboxes, and rich text editors presents additional challenges worth exploring.
  • Cross-framework implementations: The current implementation uses React and React Aria. Equivalent patterns in Vue, Svelte, and Web Components would broaden adoption.
  • Automated validation tooling: A linting rule or build-time check that verifies Layer 1 meets minimum size requirements would catch regressions before they reach production.
  • WCAG 3.0 alignment: As the new conformance model matures, the Three-Layer Architecture's relationship to Bronze/Silver/Gold ratings should be evaluated.
  • User research: Formal usability testing with motor-impaired, cognitively diverse, and assistive technology users would validate the architecture's real-world effectiveness beyond automated compliance checks.

Part X: Conclusion and Call to Action

The Decade-Long Failure

For ten years, we've watched web accessibility stagnate:

  • 2015: 98%+ of websites fail accessibility standards
  • 2025: 94.8% of websites still fail accessibility standards

Progress has been glacial. At current rates, achieving broad accessibility would take decades.

Meanwhile:

  • 1.3 billion people live with disabilities
  • 4,000+ lawsuits filed annually
  • $490 billion in disposable income among working-age adults with disabilities (American Institutes for Research)
  • Millions experience daily frustration, exclusion, and discrimination

The False Choice is Over

The Three-Layer Architecture proves that accessibility and aesthetics are not opposing forces:

The Old Paradigm (Rejected)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

            Accessibility ◄────────── CHOOSE ──────────► Aesthetics


The New Paradigm (Three-Layer Architecture)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

                    ┌──────────────────────────┐
                    │                          │
                    │   Accessibility (L1)     │
                    │         AND              │
                    │   Aesthetics (L2)        │
                    │         AND              │
                    │   Content (L3)           │
                    │                          │
                    └──────────────────────────┘

                    Different concerns.
                    Different layers.
                    Both excellent.

For Engineering Leaders

Adopt Three-Layer Architecture as your component standard:

  1. Audit existing components: Identify touch target failures
  2. Establish baseline: 44×44px minimum for Layer 1
  3. Train teams: Separation of concerns for accessibility
  4. Validate: Automated testing for each layer
  5. Document: Make the architecture explicit

For Designers

Embrace the creative freedom:

  1. Design without constraint: Visual layer is independent
  2. Trust the architecture: Touch targets are handled
  3. Focus on excellence: Accessibility is built in
  4. Think in layers: Visual appearance ≠ touch target
  5. Test with users: Validate both aesthetics and usability

For the Industry

It's time to raise the standard:

  1. Stop shipping inaccessible components
  2. Stop treating accessibility as optional
  3. Stop forcing the false choice
  4. Start building for everyone

The Path Forward

Web accessibility has been stuck for a decade because we've been approaching it wrong. We've treated it as:

  • A compliance checkbox (add ARIA attributes)
  • A styling constraint (make buttons bigger)
  • An overlay solution (JavaScript widgets)
  • An optional enhancement (accessibility mode)

None of these work.

The Three-Layer Architecture succeeds because it treats accessibility as what it actually is: an architectural concern that must be designed into the foundation of every component.

The invisible layer does the work. The visible layer gets the attention. Everyone gets what they need.


References

WebAIM Million Reports

WCAG Standards

Disability Statistics

Motor Impairment Research

Framework Documentation

European Accessibility Act

Design and UX Research

Component Patterns


Appendix A: Quick Reference Card

The Three Layers

Layer Element Purpose Size Focus
1 <button> / AriaButton Touch target 44×44px min YES
2 <span> Visual appearance Any NO
3 Children Content & state Fits L2 NO

Touch Target Requirements

Standard Minimum Level Notes
WCAG 2.2 24×24px AA Legal minimum
WCAG 2.1 44×44px AAA Recommended
Apple iOS 44×44pt - Human Interface Guidelines
Android 48×48dp - Material Design
Microsoft 44×44px - Fluent Design

Gap Mitigation Strategies

  1. Grid-aware spacing: Design grid around 44px cells with gap: 0
  2. Accept the gap: In many cases, extra spacing improves usability for everyone

Anti-Patterns to AVOID

Overlapping touch targets with z-index: Higher z-index elements steal taps intended for adjacent elements.

Shared touch targets: Wrapping multiple controls in one container breaks keyboard navigation and screen reader semantics.

Responsive touch targets: Using @media (pointer: fine) assumes device precision = user ability.

Negative margins: Creates overlapping touch targets with the same harmful effects as z-index overlap.


Appendix B: Implementation Checklist

Component Development

  • Layer 1 has min-h-[44px] min-w-[44px]
  • Layer 1 is the semantic element (<button>, etc.)
  • Layer 1 has focus ring styles
  • Layer 1 is transparent (no background)
  • Layer 2 has NO focus ring styles
  • Layer 2 visual size controlled independently
  • Layer 3 has pointer-events: none where needed
  • Icon/dot variants require aria-label
  • Loading state has aria-live="polite" announcement
  • Animations use motion-safe: prefix

Testing

  • Unit tests verify layer structure
  • Unit tests verify touch target size classes
  • Accessibility audit passes (jest-axe)
  • Visual regression tests capture appearance
  • Keyboard navigation works
  • Screen reader announces correctly
  • High contrast mode works
  • Reduced motion respected

Documentation

  • Layer architecture documented
  • visualSize prop explained
  • Accessibility requirements noted
  • Examples for all variants provided

Version History

Version Date Changes
1.0 December 2025 Initial publication
1.1 March 2026 Updated WebAIM Million 2025 figures (framework error rates, ARIA stats, error category percentages). Added European Accessibility Act section. Added WCAG 3.0 overview. Added cognitive accessibility section. Added Limitations & Future Work. Added Glossary. Corrected first impressions research attribution. Clarified 2024 lawsuit scope. Removed non-existent WCAG 2.2.1 reference.

Acknowledgements

This white paper draws on the work of countless accessibility researchers, advocates, and practitioners whose contributions have shaped the field. In particular:

  • The WebAIM team for seven years of Million reports that hold the industry accountable
  • Adobe's React Spectrum team for pioneering the separation of behaviour, state, and rendering in accessible components
  • The W3C Web Accessibility Initiative for maintaining and evolving the WCAG standard
  • The Tribepad frontend team for validating the Three-Layer Architecture in production through the Themis component library

This white paper represents research and implementation experience by Mark Basford. The Three-Layer Architecture is implemented in the Themis component library.

For related reading, see: