Ver contenido
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