Ver contenido
Guía de Implementación de Áreas Táctiles: Patrones Completos para Cumplimiento de Tamaño de Objetivo
Introducción
El dimensionamiento de áreas táctiles es crítico para usuarios con discapacidades motoras, pero implementar un tamaño adecuado va más allá de simplemente establecer ancho y alto. Esta guía cubre técnicas CSS, patrones de componentes, estrategias de espaciado e implementaciones específicas para frameworks para lograr el cumplimiento de WCAG 2.5.5 (Nivel AAA - 44×44px) y 2.5.8 (Nivel AA - 24×24px).
Comprender los matices del dimensionamiento de objetivos—incluyendo la excepción de espaciado, excepciones en línea, y cómo diferentes propiedades CSS afectan las áreas clicables—es esencial para construir interfaces verdaderamente accesibles.
Técnicas de Dimensionamiento CSS
Dimensionamiento Mínimo Básico
/* Nivel AA: 24×24 píxeles CSS mínimo */
.target-aa {
min-width: 24px;
min-height: 24px;
}
/* Nivel AAA: 44×44 píxeles CSS mínimo */
.target-aaa {
min-width: 44px;
min-height: 44px;
}
/* Recomendación Google/Material: 48×48 dp */
.target-material {
min-width: 48px;
min-height: 48px;
}
Expandiendo Elementos Pequeños con Padding
Cuando el contenido es menor que el tamaño de objetivo requerido, usa padding:
/* El icono es 16×16, necesitamos objetivo de 44×44 */
.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;
}
/* Para mínimo 24×24 con icono 16×16 */
.icon-button-aa {
padding: 4px; /* (24 - 16) / 2 = 4px */
}
Expandiendo Enlaces Sin Cambio Visual
Usa pseudo-elementos para expandir el área clicable sin cambiar el layout:
/* El texto del enlace permanece visualmente pequeño, pero el objetivo es más grande */
.expanded-link {
position: relative;
/* Estilos visuales sin cambios */
}
.expanded-link::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 44px;
min-height: 44px;
}
Usando Bordes Transparentes
Expande el área clicable mientras mantienes el diseño visual:
.button-with-expanded-target {
/* Apariencia visual */
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
/* Borde invisible expande el objetivo */
border: 10px solid transparent;
background-clip: padding-box;
margin: -10px; /* Compensar el borde */
}
Patrones de Componentes
Botones de Icono
<!-- Estructura correcta de botón de icono -->
<button
type="button"
class="icon-button"
aria-label="Cerrar diálogo"
>
<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 {
/* Asegurar tamaño mínimo de objetivo */
min-width: 44px;
min-height: 44px;
/* Centrar icono */
display: inline-flex;
align-items: center;
justify-content: center;
/* Resetear estilos de botón */
padding: 0;
border: none;
background: transparent;
cursor: pointer;
/* Estados de foco y hover */
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>
Enlaces de Navegación
<nav class="main-nav" aria-label="Navegación principal">
<ul class="nav-list">
<li><a href="/" class="nav-link">Inicio</a></li>
<li><a href="/nosotros" class="nav-link">Nosotros</a></li>
<li><a href="/servicios" class="nav-link">Servicios</a></li>
<li><a href="/contacto" class="nav-link">Contacto</a></li>
</ul>
</nav>
<style>
.nav-list {
display: flex;
gap: 8px;
list-style: none;
padding: 0;
margin: 0;
}
.nav-link {
/* Tamaño mínimo de objetivo */
display: flex;
align-items: center;
min-height: 44px;
padding: 0 16px;
/* Estilos visuales */
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>
Campos de Formulario
<div class="form-field">
<label for="email">Correo electrónico</label>
<input
type="email"
id="email"
class="form-input"
autocomplete="email"
>
</div>
<style>
.form-input {
/* Altura mínima del objetivo */
min-height: 44px;
/* Ancho completo para facilitar apuntado */
width: 100%;
/* Padding para contenido y tamaño de objetivo */
padding: 12px 16px;
/* Estilos visuales */
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px; /* Previene zoom en iOS */
}
.form-input:focus {
outline: 2px solid #007bff;
outline-offset: -2px;
border-color: #007bff;
}
</style>
Checkboxes y Radio Buttons
<div class="checkbox-field">
<input type="checkbox" id="agree" class="checkbox-input">
<label for="agree" class="checkbox-label">
Acepto los términos y condiciones
</label>
</div>
<style>
.checkbox-field {
display: flex;
align-items: flex-start;
gap: 12px;
}
/* Ocultar checkbox nativo, pero mantener accesible */
.checkbox-input {
/* Dimensionar el checkbox */
width: 24px;
height: 24px;
margin: 0;
/* O ocultar y usar estilos personalizados */
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 {
/* Hacer toda la etiqueta clicable */
min-height: 24px;
cursor: pointer;
line-height: 24px;
}
</style>
Implementación de la Excepción de Espaciado
Entendiendo la Excepción de Espaciado
WCAG 2.5.8 permite objetivos menores de 24×24 si hay suficiente espaciado:
/* Objetivos pequeños (16×16) con espaciado adecuado */
.small-target-with-spacing {
width: 16px;
height: 16px;
/* 8px de margen en todos los lados = 16px de separación */
/* Combinado con tamaño del elemento: 16 + 8 + 8 = 32px de centro a centro */
margin: 8px;
}
/* Alternativamente, usar gap en flex/grid */
.target-group {
display: flex;
gap: 24px; /* Asegura que círculos de 24×24 no se superpongan */
}
.target-group .small-target {
width: 16px;
height: 16px;
}
Explicación Visual
Objetivo A (16×16) Objetivo B (16×16)
████ ████
████ ████
◯ ← círculo 24×24 ◯ ← círculo 24×24
Con gap de 24px, los círculos no se superponen = PASA
Patrón Práctico de Espaciado
/* Paginación con indicadores pequeños */
.pagination {
display: flex;
align-items: center;
gap: 24px;
}
.pagination-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ccc;
border: none;
cursor: pointer;
/* No necesita min-width/height debido al espaciado */
}
.pagination-dot.active {
background: #007bff;
}
/* Pero asegurar que botones prev/next cumplan requisitos de tamaño */
.pagination-button {
min-width: 44px;
min-height: 44px;
}
Implementaciones en Frameworks
Componente React
// 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;
}
Componente Vue 3
<!-- 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>
Utilidades Tailwind CSS
/* 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',
},
},
},
}
/* Uso en HTML */
<button class="min-w-target-aaa min-h-target-aaa flex items-center justify-center">
<svg class="w-6 h-6"><!-- icono --></svg>
</button>
Áreas Táctiles Responsivas
Enfoque Mobile-First
/* Objetivos más grandes en dispositivos táctiles */
.interactive-element {
/* Móvil: objetivos más grandes */
min-width: 48px;
min-height: 48px;
padding: 12px;
}
/* Escritorio: puede usar objetivos más pequeños con puntero */
@media (pointer: fine) {
.interactive-element {
min-width: 32px;
min-height: 32px;
padding: 6px;
}
}
/* Dispositivos táctiles siempre obtienen objetivos más grandes */
@media (pointer: coarse) {
.interactive-element {
min-width: 48px;
min-height: 48px;
padding: 12px;
}
}
Patrón Agnóstico de Dispositivo
/* Propiedades personalizadas CSS para dimensionamiento flexible */
: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);
}
Probando Áreas Táctiles
Pruebas Automatizadas
// Test de Playwright para áreas táctiles
import { test, expect } from '@playwright/test';
test.describe('Cumplimiento de Áreas Táctiles', () => {
test('todos los botones cumplen requisitos mínimos de tamaño', async ({ page }) => {
await page.goto('/');
const buttons = await page.locator('button').all();
for (const button of buttons) {
const box = await button.boundingBox();
// Verificar mínimo 24×24 (Nivel AA)
expect(box.width).toBeGreaterThanOrEqual(24);
expect(box.height).toBeGreaterThanOrEqual(24);
}
});
test('elementos interactivos tienen espaciado adecuado', async ({ page }) => {
await page.goto('/');
const interactiveElements = await page.locator(
'button, a, input, select, textarea, [role="button"]'
).all();
// Verificar espaciado entre elementos adyacentes
for (let i = 0; i < interactiveElements.length - 1; i++) {
const box1 = await interactiveElements[i].boundingBox();
const box2 = await interactiveElements[i + 1].boundingBox();
// Calcular distancia entre centros
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)
);
// Si los elementos están cerca, deben ser lo suficientemente grandes o tener espaciado
if (distance < 48) {
expect(box1.width).toBeGreaterThanOrEqual(24);
expect(box1.height).toBeGreaterThanOrEqual(24);
}
}
});
});
Checklist de Pruebas Manuales
-
Medición con DevTools:
- Abrir DevTools del navegador
- Seleccionar elemento interactivo
- Verificar ancho y alto computado
- Verificar que cumple mínimo de 24×24
-
Verificación Visual de Espaciado:
- Buscar elementos interactivos agrupados
- Asegurar espaciado visual adecuado
- Probar con toque en dispositivo móvil
-
Verificación de Área de Clic:
- Hacer clic en los bordes de los elementos
- Verificar que el clic se registra
- Comprobar que el padding contribuye al objetivo
Errores Comunes y Soluciones
Error 1: Depender del Tamaño del Contenido
/* INCORRECTO: El tamaño del objetivo depende de la longitud del texto */
.button {
padding: 4px 8px;
/* Texto corto = botón diminuto */
}
/* CORRECTO: Aplicar tamaño mínimo */
.button {
min-width: 44px;
min-height: 44px;
padding: 8px 16px;
}
Error 2: Overflow Cortando Área del Objetivo
/* INCORRECTO: Overflow hidden corta el padding */
.container {
overflow: hidden;
}
.container .button {
padding: 20px;
margin: -10px; /* ¡El padding se corta! */
}
/* CORRECTO: Usar espaciado apropiado */
.container {
padding: 10px;
}
.container .button {
padding: 20px;
}
Error 3: Transform Afectando Área de Detección
/* INCORRECTO: Scale no aumenta el área de detección */
.icon-button {
transform: scale(2);
/* Visual es más grande, pero área de detección sin cambios */
}
/* CORRECTO: Usar dimensionamiento real */
.icon-button {
min-width: 44px;
min-height: 44px;
}
Checklist de Resumen
- [ ] Todos los elementos interactivos son al menos 24×24 píxeles CSS (Nivel AA)
- [ ] Las acciones primarias son 44×44 píxeles CSS o más (Nivel AAA)
- [ ] Los botones de icono tienen padding adecuado alrededor de los iconos
- [ ] Los objetivos adyacentes tienen espaciado mínimo de 24px (o tamaño mayor)
- [ ] Los campos de formulario tienen altura táctil mínima de 44px
- [ ] Checkboxes y radio buttons tienen tamaño de objetivo adecuado
- [ ] Los enlaces de navegación tienen área clicable suficiente
- [ ] Probado en dispositivos táctiles reales
- [ ] Dimensionamiento responsivo para diferentes métodos de entrada
- [ ] Estados de foco visibles en todos los objetivos