Detailed guide

Touch Targets Implementation Guide

View contents

Touch Targets Implementation Guide: Complete Patterns for Target Size Compliance

Introduction

Touch target sizing is critical for users with motor impairments, but implementing proper sizing goes beyond simply setting width and height. This guide covers CSS techniques, component patterns, spacing strategies, and framework-specific implementations for achieving WCAG 2.5.5 (Level AAA - 44×44px) and 2.5.8 (Level AA - 24×24px) compliance.

Understanding the nuances of target sizing—including the spacing exception, inline exceptions, and how different CSS properties affect clickable areas—is essential for building truly accessible interfaces.

CSS Sizing Techniques

Basic Minimum Sizing

/* Level AA: 24×24 CSS pixels minimum */
.target-aa {
  min-width: 24px;
  min-height: 24px;
}

/* Level AAA: 44×44 CSS pixels minimum */
.target-aaa {
  min-width: 44px;
  min-height: 44px;
}

/* Google/Material recommendation: 48×48 dp */
.target-material {
  min-width: 48px;
  min-height: 48px;
}

Expanding Small Elements with Padding

When content is smaller than the required target size, use padding:

/* Icon is 16×16, need 44×44 target */
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 14px; /* (44 - 16) / 2 = 14px */
  border: none;
  background: transparent;
  cursor: pointer;
}

.icon-button svg {
  width: 16px;
  height: 16px;
}

/* For 24×24 minimum with 16×16 icon */
.icon-button-aa {
  padding: 4px; /* (24 - 16) / 2 = 4px */
}

Use pseudo-elements to expand clickable area without changing layout:

/* Link text remains visually small, but target is larger */
.expanded-link {
  position: relative;
  /* Visual styling unchanged */
}

.expanded-link::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  min-width: 44px;
  min-height: 44px;
}

Using Transparent Borders

Expand clickable area while maintaining visual design:

.button-with-expanded-target {
  /* Visual appearance */
  padding: 8px 16px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;

  /* Invisible border expands target */
  border: 10px solid transparent;
  background-clip: padding-box;
  margin: -10px; /* Compensate for border */
}

Component Patterns

Icon Buttons

<!-- Proper icon button structure -->
<button
  type="button"
  class="icon-button"
  aria-label="Close dialog"
>
  <svg class="icon" aria-hidden="true" viewBox="0 0 24 24">
    <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
  </svg>
</button>

<style>
.icon-button {
  /* Ensure minimum target size */
  min-width: 44px;
  min-height: 44px;

  /* Center icon */
  display: inline-flex;
  align-items: center;
  justify-content: center;

  /* Reset button styles */
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;

  /* Focus and hover states */
  border-radius: 50%;
  transition: background-color 0.2s;
}

.icon-button:hover {
  background-color: rgba(0, 0, 0, 0.04);
}

.icon-button:focus-visible {
  outline: 2px solid #007bff;
  outline-offset: 2px;
}

.icon {
  width: 24px;
  height: 24px;
}
</style>
<nav class="main-nav" aria-label="Main navigation">
  <ul class="nav-list">
    <li><a href="/" class="nav-link">Home</a></li>
    <li><a href="/about" class="nav-link">About</a></li>
    <li><a href="/services" class="nav-link">Services</a></li>
    <li><a href="/contact" class="nav-link">Contact</a></li>
  </ul>
</nav>

<style>
.nav-list {
  display: flex;
  gap: 8px;
  list-style: none;
  padding: 0;
  margin: 0;
}

.nav-link {
  /* Minimum target size */
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 16px;

  /* Visual styling */
  text-decoration: none;
  color: #333;
  border-radius: 4px;
  transition: background-color 0.2s;
}

.nav-link:hover {
  background-color: #f5f5f5;
}

.nav-link:focus-visible {
  outline: 2px solid #007bff;
  outline-offset: -2px;
}
</style>

Form Inputs

<div class="form-field">
  <label for="email">Email address</label>
  <input
    type="email"
    id="email"
    class="form-input"
    autocomplete="email"
  >
</div>

<style>
.form-input {
  /* Minimum target height */
  min-height: 44px;

  /* Full width for easier targeting */
  width: 100%;

  /* Padding for content and target size */
  padding: 12px 16px;

  /* Visual styling */
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 16px; /* Prevents zoom on iOS */
}

.form-input:focus {
  outline: 2px solid #007bff;
  outline-offset: -2px;
  border-color: #007bff;
}
</style>

Checkboxes and Radio Buttons

<div class="checkbox-field">
  <input type="checkbox" id="agree" class="checkbox-input">
  <label for="agree" class="checkbox-label">
    I agree to the terms and conditions
  </label>
</div>

<style>
.checkbox-field {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

/* Hide native checkbox, but keep accessible */
.checkbox-input {
  /* Size the checkbox */
  width: 24px;
  height: 24px;
  margin: 0;

  /* Or hide and use custom styling */
  appearance: none;
  border: 2px solid #666;
  border-radius: 4px;
  cursor: pointer;
}

.checkbox-input:checked {
  background-color: #007bff;
  border-color: #007bff;
}

.checkbox-input:focus-visible {
  outline: 2px solid #007bff;
  outline-offset: 2px;
}

.checkbox-label {
  /* Make entire label clickable */
  min-height: 24px;
  cursor: pointer;
  line-height: 24px;
}
</style>

Spacing Exception Implementation

Understanding the Spacing Exception

WCAG 2.5.8 allows targets smaller than 24×24 if there’s sufficient spacing:

/* Small targets (16×16) with adequate spacing */
.small-target-with-spacing {
  width: 16px;
  height: 16px;

  /* 8px margin on all sides = 16px gap */
  /* Combined with element size: 16 + 8 + 8 = 32px center-to-center */
  margin: 8px;
}

/* Alternatively, use gap in flex/grid */
.target-group {
  display: flex;
  gap: 24px; /* Ensures 24×24 circles don't overlap */
}

.target-group .small-target {
  width: 16px;
  height: 16px;
}

Visual Explanation

Target A (16×16)        Target B (16×16)
    ████                    ████
    ████                    ████

     ◯ ← 24×24 circle       ◯ ← 24×24 circle

With 24px gap, circles don't overlap = PASS

Practical Spacing Pattern

/* Pagination with small indicators */
.pagination {
  display: flex;
  align-items: center;
  gap: 24px;
}

.pagination-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #ccc;
  border: none;
  cursor: pointer;

  /* No need for min-width/height due to spacing */
}

.pagination-dot.active {
  background: #007bff;
}

/* But make sure prev/next buttons meet size requirements */
.pagination-button {
  min-width: 44px;
  min-height: 44px;
}

Framework Implementations

React Component

// TouchTarget.tsx
import React, { forwardRef } from 'react';
import clsx from 'clsx';
import styles from './TouchTarget.module.css';

interface TouchTargetProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  size?: 'aa' | 'aaa' | 'material';
  variant?: 'icon' | 'text' | 'contained';
  children: React.ReactNode;
}

export const TouchTarget = forwardRef<HTMLButtonElement, TouchTargetProps>(
  ({ size = 'aaa', variant = 'text', children, className, ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={clsx(
          styles.touchTarget,
          styles[size],
          styles[variant],
          className
        )}
        {...props}
      >
        {children}
      </button>
    );
  }
);

// TouchTarget.module.css
.touchTarget {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  cursor: pointer;
  transition: background-color 0.2s;
}

.aa { min-width: 24px; min-height: 24px; }
.aaa { min-width: 44px; min-height: 44px; }
.material { min-width: 48px; min-height: 48px; }

.icon {
  padding: 8px;
  border-radius: 50%;
}

.text {
  padding: 8px 16px;
  border-radius: 4px;
}

.contained {
  padding: 12px 24px;
  background-color: #007bff;
  color: white;
  border-radius: 4px;
}

.touchTarget:hover { background-color: rgba(0, 0, 0, 0.04); }
.touchTarget:focus-visible {
  outline: 2px solid #007bff;
  outline-offset: 2px;
}

Vue 3 Component

<!-- TouchTarget.vue -->
<script setup lang="ts">
interface Props {
  size?: 'aa' | 'aaa' | 'material'
  variant?: 'icon' | 'text' | 'contained'
  as?: 'button' | 'a'
}

const props = withDefaults(defineProps<Props>(), {
  size: 'aaa',
  variant: 'text',
  as: 'button'
})
</script>

<template>
  <component
    :is="as"
    :class="[
      'touch-target',
      `touch-target--${size}`,
      `touch-target--${variant}`
    ]"
  >
    <slot />
  </component>
</template>

<style scoped>
.touch-target {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: transparent;
  cursor: pointer;
  text-decoration: none;
  transition: background-color 0.2s;
}

.touch-target--aa {
  min-width: 24px;
  min-height: 24px;
}

.touch-target--aaa {
  min-width: 44px;
  min-height: 44px;
}

.touch-target--material {
  min-width: 48px;
  min-height: 48px;
}

.touch-target--icon {
  padding: 8px;
  border-radius: 50%;
}

.touch-target--text {
  padding: 8px 16px;
  border-radius: 4px;
}

.touch-target--contained {
  padding: 12px 24px;
  background-color: #007bff;
  color: white;
  border-radius: 4px;
}

.touch-target:hover {
  background-color: rgba(0, 0, 0, 0.04);
}

.touch-target--contained:hover {
  background-color: #0056b3;
}

.touch-target:focus-visible {
  outline: 2px solid #007bff;
  outline-offset: 2px;
}
</style>

Tailwind CSS Utilities

/* tailwind.config.js */
module.exports = {
  theme: {
    extend: {
      minWidth: {
        'target-aa': '24px',
        'target-aaa': '44px',
        'target-material': '48px',
      },
      minHeight: {
        'target-aa': '24px',
        'target-aaa': '44px',
        'target-material': '48px',
      },
    },
  },
}

/* Usage in HTML */
<button class="min-w-target-aaa min-h-target-aaa flex items-center justify-center">
  <svg class="w-6 h-6"><!-- icon --></svg>
</button>

Responsive Touch Targets

Mobile-First Approach

/* Larger targets on touch devices */
.interactive-element {
  /* Mobile: larger targets */
  min-width: 48px;
  min-height: 48px;
  padding: 12px;
}

/* Desktop: can use smaller targets with pointer */
@media (pointer: fine) {
  .interactive-element {
    min-width: 32px;
    min-height: 32px;
    padding: 6px;
  }
}

/* Touch devices always get larger targets */
@media (pointer: coarse) {
  .interactive-element {
    min-width: 48px;
    min-height: 48px;
    padding: 12px;
  }
}

Device-Agnostic Pattern

/* CSS custom properties for flexible sizing */
:root {
  --target-size-min: 44px;
  --target-padding: 10px;
}

@media (pointer: fine) {
  :root {
    --target-size-min: 32px;
    --target-padding: 6px;
  }
}

.touch-target {
  min-width: var(--target-size-min);
  min-height: var(--target-size-min);
  padding: var(--target-padding);
}

Testing Touch Targets

Automated Testing

// Playwright test for touch targets
import { test, expect } from '@playwright/test';

test.describe('Touch Target Compliance', () => {
  test('all buttons meet minimum size requirements', async ({ page }) => {
    await page.goto('/');

    const buttons = await page.locator('button').all();

    for (const button of buttons) {
      const box = await button.boundingBox();

      // Check minimum 24×24 (Level AA)
      expect(box.width).toBeGreaterThanOrEqual(24);
      expect(box.height).toBeGreaterThanOrEqual(24);
    }
  });

  test('interactive elements have adequate spacing', async ({ page }) => {
    await page.goto('/');

    const interactiveElements = await page.locator(
      'button, a, input, select, textarea, [role="button"]'
    ).all();

    // Check spacing between adjacent elements
    for (let i = 0; i < interactiveElements.length - 1; i++) {
      const box1 = await interactiveElements[i].boundingBox();
      const box2 = await interactiveElements[i + 1].boundingBox();

      // Calculate distance between centers
      const center1 = { x: box1.x + box1.width / 2, y: box1.y + box1.height / 2 };
      const center2 = { x: box2.x + box2.width / 2, y: box2.y + box2.height / 2 };

      const distance = Math.sqrt(
        Math.pow(center2.x - center1.x, 2) +
        Math.pow(center2.y - center1.y, 2)
      );

      // If elements are close, they should be large enough or have spacing
      if (distance < 48) {
        expect(box1.width).toBeGreaterThanOrEqual(24);
        expect(box1.height).toBeGreaterThanOrEqual(24);
      }
    }
  });
});

Manual Testing Checklist

  1. DevTools Measurement:

    • Open browser DevTools
    • Select interactive element
    • Check computed width and height
    • Verify meets 24×24 minimum
  2. Visual Spacing Check:

    • Look for clustered interactive elements
    • Ensure adequate visual spacing
    • Test with touch on mobile device
  3. Click Area Verification:

    • Click at edges of elements
    • Verify click registers
    • Check padding contributes to target

Common Mistakes and Fixes

Mistake 1: Relying on Content Size

/* WRONG: Target size depends on text length */
.button {
  padding: 4px 8px;
  /* Short text = tiny button */
}

/* CORRECT: Enforce minimum size */
.button {
  min-width: 44px;
  min-height: 44px;
  padding: 8px 16px;
}

Mistake 2: Overflow Cutting Target Area

/* WRONG: Overflow hidden cuts padding */
.container {
  overflow: hidden;
}

.container .button {
  padding: 20px;
  margin: -10px; /* Padding cut off! */
}

/* CORRECT: Use proper spacing */
.container {
  padding: 10px;
}

.container .button {
  padding: 20px;
}

Mistake 3: Transform Affecting Hit Area

/* WRONG: Scale doesn't increase hit area */
.icon-button {
  transform: scale(2);
  /* Visual is larger, but hit area unchanged */
}

/* CORRECT: Use actual sizing */
.icon-button {
  min-width: 44px;
  min-height: 44px;
}

Summary Checklist

  • [ ] All interactive elements are at least 24×24 CSS pixels (Level AA)
  • [ ] Primary actions are 44×44 CSS pixels or larger (Level AAA)
  • [ ] Icon buttons have adequate padding around icons
  • [ ] Adjacent targets have minimum 24px spacing (or larger size)
  • [ ] Form inputs have minimum 44px touch height
  • [ ] Checkboxes and radio buttons have adequate target size
  • [ ] Navigation links have sufficient clickable area
  • [ ] Tested on actual touch devices
  • [ ] Responsive sizing for different input methods
  • [ ] Focus states visible on all targets

References

  1. W3C - WCAG 2.2 SC 2.5.5 Target Size (Enhanced)
  2. W3C - WCAG 2.2 SC 2.5.8 Target Size (Minimum)
  3. W3C - Technique C42: Using min-height and min-width
  4. Apple - Human Interface Guidelines: Accessibility
  5. Google - Material Design 3: Accessibility

Related articles

Related version

Introduction

Touch Targets Explained

Touch targets are the interactive areas users can tap, click, or touch to activate controls like buttons, links, and form inputs

Category hub

Hub

Wcag Compliance Hub

Web accessibility ensures that websites and applications can be used by everyone, including people with disabilities