Implements mobile web accessibility with proper touch target sizing (44/48pt minimum), gesture alternatives, viewport configuration for zoom, orientation support, and screen reader testing methodology for VoiceOver/TalkBack.
Scanned 9/23/2026
npx -y skills add paulpas/agent-skill-router --skill mobile-web-accessibility-touch-targets --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Mobile Web Accessibility Touch Targets?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/paulpas-mobile-web-accessibility-touch-targets)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: mobile-web-accessibility-touch-targets
description: "Implements mobile web accessibility with proper touch target sizing (44/48pt minimum), gesture alternatives, viewport configuration for zoom, orientation support, and screen reader testing methodology for VoiceOver/TalkBack."
license: MIT
compatibility: opencode
metadata:
version: "1.0.0"
domain: coding
role: implementation
scope: implementation
output-format: code
content-types:
- code
- patterns
- guidance
triggers: mobile accessibility, touch targets, VoiceOver, TalkBack, gesture alternatives, viewport meta, touch events, screen reader testing
related-skills: wcag-21-aa-fundamentals, keyboard-navigation-focus-management
archetypes:
- tactical
- enforcement
anti_triggers:
- desktop-only
- brainstorming
- backend
response_profile:
verbosity: low
directive_strength: high
abstraction_level: operational
---
# Mobile Web Accessibility: Touch Targets & Screen Reader Testing
Implements touch-accessible web experiences with proper target sizing (WCAG 2.5.8: 24px minimum, 44/48pt recommended), gesture alternatives for swipe/pinch, viewport meta configuration, orientation support, and mobile screen reader testing (VoiceOver on iOS, TalkBack on Android). Load when building mobile-first interfaces, ensuring accessibility for touch devices, or testing with mobile screen readers.
## TL;DR Checklist
- [ ] Touch targets: minimum 44×44px (CSS pixels) or 24×24px with 8px spacing
- [ ] 8px gap minimum between adjacent touch targets
- [ ] Provide gesture alternatives: single-tap for swipe/pinch/long-press
- [ ] Viewport meta: `viewport-fit=cover, user-scalable=yes` (never disable zoom)
- [ ] Test reflow at 320px width (mobile landscape on small phone)
- [ ] Support device orientation (portrait/landscape/reverse)
- [ ] Test with VoiceOver (iOS) and TalkBack (Android)
- [ ] Text resizable to 200% without loss of functionality
- [ ] Touch events and pointer events both supported
---
## When to Use
Use this skill when:
- Building responsive mobile interfaces
- Ensuring touch targets meet WCAG 2.5.8 requirements
- Implementing gesture-based interactions (swipe, pinch, drag)
- Testing with mobile screen readers (VoiceOver/TalkBack)
- Optimizing for small screens (320px width)
- Supporting multiple device orientations
---
## When NOT to Use
Avoid this skill for:
- Desktop-only experiences (use general accessibility skills)
- Touch event technical reference (use MDN/browser docs)
- Mobile UI design (use design/UX skills)
- iOS/Android native app development (different platforms)
---
## Core Workflow
1. **Size Touch Targets** — Ensure all interactive elements 44×44px minimum (CSS pixels)
2. **Add Spacing** — Maintain 8px gap between adjacent targets
3. **Configure Viewport** — Set viewport meta for zoom support and reflow
4. **Support Gestures** — Provide single-tap alternatives to swipe/pinch
5. **Test Orientation** — Verify portrait/landscape work correctly
6. **Test with Screen Readers** — VoiceOver (iOS), TalkBack (Android)
7. **Test at 320px** — Verify mobile landscape small phone works
8. **Test Text Resizing** — 200% text scale without overflow/loss
---
## Touch Target Sizing
### Minimum Size Requirement
WCAG 2.1 SC 2.5.5 (Enhanced, AAA): **44×44 CSS pixels** minimum.
WCAG 2.2 SC 2.5.8 (AA): **24×24 CSS pixels** minimum, but with **8px gap** between adjacent targets.
**Recommendation for AA compliance (most common):**
- **44×44px minimum** (no gap math needed)
- **48×48px recommended** (larger on mobile)
- **8px gap** between adjacent targets
### Implementation
```css
/* ✅ GOOD: Touch target sizing -->
button,
[role="button"],
input[type="checkbox"],
input[type="radio"] {
min-height: 44px;
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
}
/* Form inputs -->
input[type="text"],
input[type="email"],
input[type="password"],
textarea,
select {
min-height: 44px;
padding: 8px 12px;
}
/* Icon buttons with implicit padding -->
.icon-button {
min-height: 48px;
min-width: 48px;
padding: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Toolbar with spacing between items -->
.toolbar {
display: flex;
gap: 8px; /* Minimum gap between adjacent targets */
}
/* ❌ BAD: Too small for touch -->
button { height: 32px; width: 32px; }
/* ❌ BAD: No gap between targets -->
.nav-item {
display: inline-block;
width: 40px;
/* Adjacent items touch — hard to target */
}
```
### Touch Target Calculator
For elements smaller than 44px, use 8px gap:
```
Target 1: 32px
Target 2: 32px
Gap: 8px
Total space: 32 + 8 + 32 = 72px (or stack vertically)
```
For multiple adjacent targets:
```html
<!-- ✅ GOOD: Spaced buttons for touch -->
<div style="display: flex; gap: 8px;">
<button>Action 1</button>
<button>Action 2</button>
<button>Action 3</button>
</div>
<!-- ✅ GOOD: Icon buttons with implicit padding -->
<div style="display: flex; gap: 4px;">
<button style="padding: 12px;">
<svg><!-- icon --></svg>
</button>
<button style="padding: 12px;">
<svg><!-- icon --></svg>
</button>
</div>
<!-- ❌ BAD: Targets too close -->
<div style="display: flex;">
<button style="padding: 2px;">A</button>
<button style="padding: 2px;">B</button>
</div>
```
---
## Viewport Configuration
### Viewport Meta Tag
```html
<!-- ✅ GOOD: Allow user zoom, support full-width layouts -->
<meta name="viewport" content="
width=device-width,
initial-scale=1,
viewport-fit=cover,
user-scalable=yes,
maximum-scale=5
">
<!-- ❌ BAD: Disable zoom (violates WCAG) -->
<meta name="viewport" content="
width=device-width,
initial-scale=1,
user-scalable=no,
maximum-scale=1
">
```
### Viewport Properties Explained
| Property | Value | Purpose |
|----------|-------|---------|
| `width` | `device-width` | Match device width (responsive) |
| `initial-scale` | `1` | Don't pre-zoom (let user decide) |
| `viewport-fit` | `cover` | Use notch/safe area (iPhone X+) |
| `user-scalable` | `yes` | Allow pinch-to-zoom (required) |
| `maximum-scale` | `5` | Allow zoom to 500% minimum |
---
## Responsive Design at Small Viewports
### Testing at 320px Width
```css
/* Mobile-first approach -->
@media (min-width: 320px) {
/* Base styles: single column, full-width buttons */
body { font-size: 16px; }
button { width: 100%; }
.card { width: 100%; }
}
@media (min-width: 640px) {
/* Tablet: two-column layout */
.grid { display: grid; grid-template-columns: 1fr 1fr; }
}
@media (min-width: 1024px) {
/* Desktop: wider layout */
.grid { grid-template-columns: repeat(3, 1fr); }
}
/* Ensure no horizontal scrolling at any width -->
* {
max-width: 100%;
overflow-x: hidden; /* Last resort, fix layout instead */
}
```
### Orientation Support
```css
/* Detect portrait vs landscape */
@media (orientation: portrait) {
/* Portrait-specific styles */
.sidebar { display: block; }
}
@media (orientation: landscape) {
/* Landscape-specific styles */
.sidebar { display: none; } /* Or sidebar layout */
}
/* Safe area support (notches on modern phones) */
@supports (padding: max(0px)) {
body {
padding-left: max(16px, env(safe-area-inset-left));
padding-right: max(16px, env(safe-area-inset-right));
padding-top: max(16px, env(safe-area-inset-top));
}
}
```
---
## Gesture Implementation Patterns
### Pattern 1: Swipe-to-Dismiss with Tap Alternative
```typescript
import { useState, useRef, useEffect } from 'react';
interface SwipeDismissProps {
onDismiss: () => void;
children: React.ReactNode;
}
export function SwipeDismiss({ onDismiss, children }: SwipeDismissProps) {
const ref = useRef<HTMLDivElement>(null);
const [startX, setStartX] = useState(0);
const handleTouchStart = (e: React.TouchEvent) => {
setStartX(e.touches[0].clientX);
};
const handleTouchEnd = (e: React.TouchEvent) => {
const endX = e.changedTouches[0].clientX;
const diff = startX - endX;
// Swipe left > 100px to dismiss
if (diff > 100) {
onDismiss();
}
};
return (
<div
ref={ref}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
className="swipe-container"
>
<div className="swipe-content">{children}</div>
{/* Tap to dismiss alternative (required for accessibility) */}
<button
className="dismiss-button"
aria-label="Dismiss notification"
onClick={onDismiss}
>
✕
</button>
</div>
);
}
// Usage
export function NotificationDemo() {
const [show, setShow] = useState(true);
return (
show && (
<SwipeDismiss onDismiss={() => setShow(false)}>
<p>Swipe left or tap X to dismiss</p>
</SwipeDismiss>
)
);
}
```
### Pattern 2: Pinch-to-Zoom Alternative
```typescript
import { useState, useRef } from 'react';
interface PinchZoomProps {
children: React.ReactNode;
}
export function PinchZoom({ children }: PinchZoomProps) {
const [scale, setScale] = useState(1);
const startDistanceRef = useRef(0);
const getDistance = (touches: TouchList) => {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.hypot(dx, dy);
};
const handleTouchStart = (e: React.TouchEvent) => {
if (e.touches.length === 2) {
startDistanceRef.current = getDistance(e.touches);
}
};
const handleTouchMove = (e: React.TouchEvent) => {
if (e.touches.length === 2 && startDistanceRef.current > 0) {
const currentDistance = getDistance(e.touches);
const scaleChange = currentDistance / startDistanceRef.current;
// Limit zoom between 1x and 3x
const newScale = Math.min(3, Math.max(1, scale * scaleChange));
setScale(newScale);
startDistanceRef.current = currentDistance;
}
};
const handleTouchEnd = () => {
startDistanceRef.current = 0;
};
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
className="pinch-container"
style={{ transform: `scale(${scale})` }}
>
{children}
{/* Zoom buttons for non-touch or accessibility */}
<div className="zoom-controls">
<button onClick={() => setScale(Math.max(1, scale - 0.1))} aria-label="Zoom out">
−
</button>
<span>{Math.round(scale * 100)}%</span>
<button onClick={() => setScale(Math.min(3, scale + 0.1))} aria-label="Zoom in">
+
</button>
</div>
</div>
);
}
```
### Pattern 3: Long-Press with Tap Alternative
```typescript
import { useState, useRef } from 'react';
interface LongPressProps {
onLongPress: () => void;
onTap: () => void;
duration?: number;
children: React.ReactNode;
}
export function LongPressButton({
onLongPress,
onTap,
duration = 500,
children,
}: LongPressProps) {
const timerRef = useRef<NodeJS.Timeout>();
const [isPressed, setIsPressed] = useState(false);
const handleTouchStart = () => {
setIsPressed(true);
timerRef.current = setTimeout(() => {
onLongPress();
setIsPressed(false);
}, duration);
};
const handleTouchEnd = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
if (isPressed) {
onTap();
}
setIsPressed(false);
};
return (
<div
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
className={`long-press-button ${isPressed ? 'pressed' : ''}`}
>
{children}
{/* Explicit button for non-touch/accessibility */}
<button onClick={onLongPress} aria-label="Show more options">
⋮
</button>
</div>
);
}
```
---
## Mobile Screen Reader Testing
### VoiceOver (iOS)
**Enable:** Settings → Accessibility → VoiceOver (toggle on)
**Essential gestures:**
- **Single tap**: Select/focus element
- **Two-finger Z**: Undo
- **Two-finger double-tap**: Activate selected element
- **Swipe up/down**: Navigate by heading/landmark/etc.
- **Flick right/left**: Next/previous item
- **Rotor**: Swipe up then right/left to navigate by type
**Testing checklist:**
- [ ] All interactive elements focusable (two-finger double-tap activates)
- [ ] Form labels announced
- [ ] Error messages announced as alerts
- [ ] Headings form logical outline
- [ ] Landmarks (nav, main, footer) navigate correctly
- [ ] Focus order logical
**Test commands:**
```bash
# Simulate VoiceOver focus for visual inspection
# (when VoiceOver on, iOS announces everything)
# Test actual device for authentic experience
```
### TalkBack (Android)
**Enable:** Settings → Accessibility → TalkBack (toggle on)
**Essential gestures:**
- **Single tap**: Focus element
- **Double tap**: Activate focused element
- **Swipe left/right**: Next/previous item
- **Swipe down/up**: Depends on reading control mode
- **Two-finger tap**: Toggle reading/pause
- **Swipe down then right**: Open reading controls
**Testing checklist:**
- [ ] Touch exploration works (touch anywhere, TalkBack announces)
- [ ] Button/link activation works (double-tap to activate)
- [ ] Form inputs readable and writable
- [ ] Page structure clear (headings, sections announced)
- [ ] No info conveyed by color alone
### Testing Methodology
1. **Enable screen reader** (VoiceOver or TalkBack)
2. **Close eyes or use external screen reader** (more authentic)
3. **Navigate entire page** using screen reader only
4. **Verify announcements**:
- Element names (button "Submit", heading "Contact Us")
- Element types (button, link, heading)
- Element states (selected, disabled, required)
- Form field associations (label + input = one unit)
5. **Test interactive features** (forms, menus, dialogs)
6. **Verify no screen reader only content** (all content accessible)
---
## Touch Event Patterns
### Pattern: Touch Events with Mouse Fallback
```typescript
interface TouchableProps {
onPress: () => void;
children: React.ReactNode;
}
export function Touchable({ onPress, children }: TouchableProps) {
return (
<button
onClick={onPress} // Mouse/pointer support
onTouchEnd={onPress} // Touch support
className="touchable"
>
{children}
</button>
);
}
```
### Pattern: Pointer Events (Modern Approach)
```typescript
interface PointerTouchProps {
onPointerDown: () => void;
onPointerUp: () => void;
children: React.ReactNode;
}
export function PointerTouch({
onPointerDown,
onPointerUp,
children,
}: PointerTouchProps) {
return (
<button
onPointerDown={onPointerDown} // Unifies mouse/touch/pen
onPointerUp={onPointerUp}
className="pointer-touchable"
>
{children}
</button>
);
}
```
---
## Constraints
### MUST DO
- Ensure all touch targets minimum 44×44px (CSS pixels)
- Maintain 8px gap between adjacent interactive elements
- Never disable user zoom (viewport `user-scalable=yes`)
- Support device rotation (portrait/landscape)
- Test text resize to 200% without loss of functionality
- Provide single-tap alternative to swipe/pinch/long-press
- Test with VoiceOver (iOS) and TalkBack (Android)
- Ensure minimum 16px font size for readable text
- Support orientation lock notification (Safari iOS)
### MUST NOT DO
- Set `user-scalable=no` (disables accessibility zoom)
- Use `maximum-scale=1` (prevents zoom)
- Create targets smaller than 44×44px without spacing
- Implement gesture-only interactions without fallbacks
- Assume touch behavior works same as mouse
- Skip mobile screen reader testing
- Ignore safe area on notched devices
- Leave app unresponsive on landscape/portrait switch
---
## Mobile Accessibility Checklist
**Layout & Sizing:**
- [ ] Touch targets 44×44px minimum
- [ ] 8px spacing between adjacent targets
- [ ] No horizontal scrolling at 320px width
- [ ] Reflows correctly at 200% text zoom
- [ ] Safe area respected (notches, home indicators)
**Interaction:**
- [ ] All gestures have single-tap alternatives
- [ ] Swipe → button or menu
- [ ] Pinch-zoom → zoom buttons
- [ ] Long-press → tap + hold visual or button
- [ ] Touch events trigger on `touchend`, not `touchstart`
**Screen Reader (VoiceOver/TalkBack):**
- [ ] All content discoverable
- [ ] Forms labeled and grouped
- [ ] Errors announced as alerts
- [ ] Focus order logical
- [ ] Landmarks navigate structure
**Zoom & Text:**
- [ ] User can zoom to 200% minimum
- [ ] Text resizable in browser settings
- [ ] No fixed viewport scales
- [ ] Content doesn't overflow when zoomed
---
## Testing Tools
| Tool | Platform | Purpose |
|------|----------|---------|
| **VoiceOver** | iOS/macOS | Apple screen reader (built-in) |
| **TalkBack** | Android | Google screen reader (built-in) |
| **axe DevTools** | Chrome/Firefox | Accessibility audit on mobile |
| **WAVE** | Firefox | Visual accessibility feedback |
| **Chrome DevTools** | Chrome | Device emulation, touch simulation |
| **Lighthouse** | Chrome | Mobile accessibility audit |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!