Saltar al contenido principal

Prompts para Testear Accesibilidad con IA: WCAG, axe y Casos Reales

10 prompts para revisar accesibilidad con IA: WCAG 2.2, navegación por teclado, roles ARIA, contraste y auditorías automáticas con axe-core.

Fran Cobos 24 min de lectura 4655 palabras

Tabla de contenidos

Llevo años haciendo auditorías de accesibilidad y el 80% de los hallazgos son siempre los mismos. No los complicados: los mismos. Botones que son <div>. Formularios sin <label>. Modales donde el foco se pierde en el limbo. El clásico de siempre, que ni una sola herramienta automática detecta.

El día que entendí esto cambió mi forma de trabajar. Dejé de pensar en “pasar axe” y empecé a pensar en “usar la app como lo que es: una persona que usa el teclado, no un ratón, y que necesita que la pantalla le diga qué está pasando”.

axe-core sigue siendo imprescindible: te da una red de seguridad barata y objetiva. Pero es el suelo, no el techo. Encima hace falta criterio, y el criterio se escribe muy bien en un prompt.

Este artículo te da los 10 prompts que uso para auditar accesibilidad de una app, cómo convertirlos en un proceso que no se quede en un informe que nadie lee, y qué partes de la auditoría no las puede hacer una máquina.

Si estás montando la UI desde cero, la guía de shadcn/ui cubre bastante de esto, pero las caveats de cada componente hay que revisarlas.

Y si lo que quieres es que la auditoría corra sola en cada push, el prompt 9 te monta el CI entero. Si lo que te pasa es que el equipo mete tests pero se rompen igual, eso es otro tema: programar con IA sin arruinarte cubre por qué los tests generados sin criterio no valen nada.

Antes de pegar el primer prompt: qué NO puede hacer ninguna herramienta

Conviene tenerlo claro desde el minuto uno, porque es donde se pierde el tiempo.

El estudio más citado sobre accesibilidad automática (Fain, 2018, y trabajo posterior de Deque) estima que las herramientas automáticas detectan en torno al 30% de los problemas WCAG. El otro 70% requiere criterio humano. Y buena parte de ese 70% no es “difícil de automatizar”, es literalmente imposible con análisis estático:

  • Si el alt de una imagen de un gráfico dice “gráfico”, el linter pasa. Para un usuario de lector de pantalla, no dice absolutamente nada. Un humano tiene que saber qué información era esencial en ese gráfico.
  • Si un <select> tiene un <label> asociado y todas sus opciones, axe dice que está bien. Si el flujo obliga a elegir “Ninguno” y luego salir del campo sin haber elegido nada real, la validación solo aparece al enviar y se pierde todo lo escrito antes. Eso es un fallo de nivel A que ningún linter ve.
  • Si tu app entera funciona por teclado pero el foco salta del último elemento al body sin llegar al siguiente control, no hay ningún error detectable: los controles son correctos, la secuencia simplemente está mal.

Por tanto: automatizado para lo mecánico, IA para el razonamiento, persona para el juicio.

Reglas de la casa antes de auditar nada

1. Decide el nivel antes de empezar

WCAG tiene tres niveles: A (mínimo, lo imposible de no cumplir), AA (el estándar real, casi todo el mundo apunta aquí) y AAA (accesibilidad aluxe, raro y a veces imposible). Sin decidir esto, la auditoría se convierte en una lista interminable de “sería mejor que…”. Escribe el objetivo en el README y audita contra eso.

2. Un criterio de WCAG, un hallazgo

Cada hallazgo lleva su código (por ejemplo 1.4.3 Contraste mínimo, nivel AA) y una línea de código o un flujo concreto. Sin eso, el informe se queda en “debería ser más accesible” y no se puede priorizar ni cerrar.

3. Prioriza por impacto de usuario, no por esfuerzo

Un formulario con label ausente rompe a gente todos los días. Un contraste de 4.4:1 en un texto secundario no le rompe a nadie. Prioriza por lo que bloquea tareas, no por lo que sale más en la herramienta.

4. Prueba con teclado antes de leer código

Antes de pedirle nada a la IA, dedica veinte minutos a recorrer la app con el tabulador y hacer Shift+Tab. Lo que descubras ahí es material directo para el prompt, y las descripciones de flujo que le des serán mucho más útiles que un volcado de HTML.

5. Usa el navegador, no el diff

Un fragmento de componente no dice cómo se comporta el modal en producción. Pasa la URL y la ruta de los archivos, y exige que el análisis distinga “esto no se puede saber leyendo el código” de “esto está mal”.

1. Auditoría WCAG 2.2 completa desde el código

Prompt:

Audita estos archivos de mi aplicación React/Next.js contra WCAG 2.2 nivel AA:

RUTAS: [PEGAR RUTAS O LISTA DE ARCHIVOS]
STACK: [Next.js 15 / React 19, Tailwind 3.4, shadcn/ui]
UBICACIÓN DE LA UI: [PEGAR HTML RENDERIZADO O DESCRIPCIÓN]

Para cada archivo, revisa TODOS estos criterios y di si pasa, falla o
"no determinable leyendo código":

A: 1.3.1 Info y relaciones, 2.1.1 Teclado, 2.4.1 Evitar bloques, 2.4.2 Página
   con título, 2.4.3 Orden del foco, 2.4.4 Propósito del enlace, 3.1.1 Idioma
   de la página, 4.1.2 Nombre, rol, valor
AA: 1.4.1 Uso del color, 1.4.3 Contraste mínimo, 1.4.4 Redimensionar texto,
   1.4.10 Reflow, 1.4.11 Non-text Contrast, 1.4.12 Espaciado de texto,
   1.4.13 Hover o Focus, 2.4.6 Encabezados y etiquetas, 2.4.7 Focus Visible,
   2.5.3 Label in Name, 3.2.3 Navegación coherente, 3.3.1 Identificación de
   errores, 3.3.2 Etiquetas o instrucciones, 4.1.3 Mensajes de estado
2.2: 2.4.11 Focus Not Obscured (AA), 2.5.7 Dragging Movements (AA),
   2.5.8 Target Size (AA, mínimo 24x24 CSS px)

Formato de salida por hallazgo:
- criterio (código + nombre)
- nivel (A/AA)
- archivo:línea
- qué falla, en una frase
- a quién afecta (teclado / lector de pantalla / baja visión / motor /
 Ye)
- fix exacto, en código
- veredicto: ¿puedes confirmarlo leyendo el código o necesitas probarlo?

Si algo no lo puedes saber leyendo el código, dilo explícitamente en vez
de suponer. Devuelve tabla + detalle por hallazgo.

Por qué funciona: darle los códigos de criterio exactos evita la respuesta genérica de “deberías añadir roles ARIA”. Y la instrucción explícita de declarar “no determinable” es la que evita el invento, que es el problema real de auditar con un modelo.

2. Diseñar la auditoría por recorrido de teclado

Este es el prompt que más findings reales produce, porque el teclado es donde fallan las apps.

Prompt:

Quiero una auditoría de accesibilidad por teclado de esta aplicación:

RUTAS DE NAVEGACIÓN: [PEGAR URLS, p.ej. /, /carrito, /checkout, /cuenta]
COMPONENTES INTERACTIVOS: [MODAL, COMBO CUSTOM, TABLA, DROPDOWN, DATE PICKER]

Para cada flujo, simula mentalmente el recorrido completo de teclado y
responde:

1. ¿Qué es el primer elemento que recibe el foco al cargar la página?
   ¿Debería ser el skip link al contenido principal?
2. Al tabular, ¿el foco recorre en el orden visual? ¿Qué elemento se salta
   o repite?
3. ¿En qué punto el foco entra en el componente y cómo vuelve a salir?
   ¿Se queda atrapado (focus trap) o se pierde?
4. Si el componente es un overlay (modal, popover, dropdown):
   - ¿el foco se mueve al abrirlo?
   - ¿Escape lo cierra?
   - ¿al cerrarlo, el foco vuelve al elemento que lo abrió? ¿Qué pasa si
     ese elemento ya no existe (navegaste a otra vista)?
5. ¿Hay algún elemento con tabindex positivo? ¿Qué efecto tiene en el orden?
6. ¿El anillo de foco (focus-visible) se ve sobre TODOS los fondos? ¿Se
   distingue cuando el elemento está sobre una imagen o un color parecido?
7. ¿Se puede usar todo sin soltar el teclado, incluido arrastrar para
   escribir y para reordenar (WCAG 2.5.7)?

Devuelve un mapa de flujo de foco por cada ruta, escrito como lista ordenada
de pasos, marcando en rojo donde el recorrido se rompe. Al final, la lista
de correcciones en orden de prioridad.

Por qué funciona: el modelo sabe cómo debería comportarse un focus trap correcto, pero solo lo razona si le haces preguntas separadas por comportamiento. Enumerar las seis propiedades del overlay es lo que convierte el análisis en algo accionable.

3. Auditar roles ARIA contra HTML semántico

Casi todas las apps que he auditado tienen más ARIA del que necesitan, y ahí está el problema.

Prompt:

Compara estas dos implementaciones equivalentes de un selector múltiple:

VERSIÓN CON HTML SEMÁNTICO:
[PEGAR CÓDIGO A]

VERSIÓN CON ARIA:
[PEGAR CÓDIGO B]

Analiza:

1. ¿Hay ARIA redundante o contradictorio en la versión B? Lista cada
   atributo que duplica un elemento nativo (aria-label en un <button>, role
   en un <nav>, etc.) y por qué es ruido o directamente dañino.
2. ¿Algún role no coincide con el comportamiento real? Comprueba
   especialmente: combobox/listbox, tablist/tab, dialog, alertdialog,
   menu/menuitem, switch vs checkbox.
3. ¿Falta aria-live o role="alert" en algún mensaje de estado? ¿Hay algún
   aria-live en un contenedor que cambia tan a menudo que satura al lector
   de pantalla (por ejemplo, un contador que se actualiza por teclado)?
4. ¿aria-describedby / aria-errormessage apuntan a elementos que existen en
   el momento en que se asigna el atributo? ¿Se actualizan de forma dinámica
   sin perder la asociación?
5. ¿aria-labelledby apunta al id correcto cuando el componente se repite en
   lista?

Devuelve tabla de atributos: atributo | ¿necesario? | por qué | alternativa
semántica recomendada. Al final, la versión corregida de B usando el
mínimo número de ARIA posible.

Por qué funciona: el principle “no ARIA es mejor que ARIA mala” es conocido pero se viola en todas partes. Pedir explícitamente la versión corregida con mínimo ARIA convierte el análisis en un refactor concreto.

4. Calcular contraste real de la paleta

El linter calcula contraste sobre los colores finales. Lo que falla en la práctica es la cascada de Tailwind con opacidad y estados hover.

Prompt:

Esta es la paleta de mi aplicación Tailwind:

[PEGAR tailwind.config.js con los colores de marca y las clases dark:]

Y estas son las combinaciones que uso en componentes reales:

[PEGAR LISTA DE CLASES, p.ej. "text-slate-500 bg-white",
"text-white bg-brand-600 hover:bg-brand-700", "text-brand-300 bg-slate-900"]

Para cada combinación:
1. Calcula el ratio de contraste WCAG en modo claro Y en modo oscuro,
   teniendo en cuenta que Tailwind resuelve /50, /70, /90 con alpha sobre el
   fondo del padre.
2. Aplica los umbrales correctos: texto normal 4.5:1 (AA), texto normal
   ≥7:1 (AAA), texto grande (≥18.66px bold o ≥24px) 3:1, componentes de
   UI y bordes con significado 3:1 (1.4.11, nivel AA).
3. Indica si el estado hover o focus baja de 4.5:1 aunque el estado base
   pase. Esto es el fallo más común y axe no lo ve porque solo audita el
   estado por defecto.
4. Propón la corrección mínima: ¿cambiar el color del texto, del fondo, o
   subir la opacidad? Da el valor en HEX final, no "un gris más oscuro".

Devuelve tabla con: combinación | ratio claro | ratio oscuro | ratio hover |
umbral aplicable | veredicto | HEX corregido.

Por qué funciona: el punto clave es el estado hover. El 70% de las interfaces pasan el test en reposo y fallan en :hover, porque el color del fondo cambia y el texto se queda igual. Ningún analizador automático captura esa transición.

5. Generar alt text útil y nombres accesibles

Prompt:

Estas son las imágenes y los controles de mi aplicación:

[PEGAR LISTA CON: ruta o nombre, contexto de uso, contenido visual]

Para cada una, escribe:
- alt o aria-label siguiendo la regla de oro: el alt describe la FUNCIÓN
  de la imagen en este contexto, no su apariencia. Si la imagen es
  decorativa: alt="" (vacío) y aria-hidden="true". Si es un enlace o botón
  con imagen dentro, el nombre accesible va en el elemento que lo contiene,
  no en la imagen.
- Para imágenes con información (gráficos, diagramas, capturas): un alt
  breve + una descripción larga referenciada con aria-describedby.
- Para gráficos de datos: indica qué dato NO puede perderse en el alt.

Comprueba además los nombres accesibles de todos los enlaces y botones:
- Los enlaces "haz clic aquí" / "leer más" / "pincha" son fallo de 2.4.4.
  Reescribe cada uno con el destino real.
- Los botones solo con icono necesitan aria-label Y deben cumplir
  2.5.3 Label in Name: el texto visible tiene que estar contenido en el
  nombre accesible (si hay texto visible).

Devuelve el código JSX corregido de cada componente.

Por qué funciona: el “vacío significa decorativa” es el punto que casi nadie aplica bien. Un alt="imagen decorativa" es peor que un alt="", porque un lector de pantalla leerá esas dos palabras sin ningún contenido útil.

6. Revisar formularios: errores, validación y foco

Prompt:

Este es mi componente de formulario:

[PEGAR CÓDIGO DEL FORMULARIO]

Audítalo contra 3.3.1 (Identificación de errores, A), 3.3.2 (Etiquetas o
instrucciones, A), 3.3.3 (Sugerencia al corregir, AA), 2.4.6 (Etiquetas e
instrucciones, AA) y 3.3.7 Redundant Entry (2.2, A).

Responde punto por punto:
1. ¿Cada campo tiene label asociado por htmlFor/id? ¿Qué pasa con los
   campos de tipo hidden y los placeholder como único identificador?
2. Si la validación falla: ¿el error se anuncia con role="alert" o
   aria-live="assertive"? ¿el foco se mueve al primer campo con error o al
   resumen de errores?
3. ¿El mensaje de error está asociado al campo con aria-describedby o
   aria-errormessage, y se actualiza dinámicamente?
4. ¿El texto del error identifica el problema Y cómo corregirlo? (Ej: "Formato
   incorrecto" → "El IBAN debe empezar por ES y tener 24 caracteres")
5. ¿Se pierde todo lo escrito cuando hay error de servidor? (3.3.7 y
   criterio de usabilidad)
6. ¿Hay campos obligatorios marcados solo con asterisco? ¿Tienen también
   aria-required o required?
7. ¿El autocomplete está puesto en los datos personales y de pago?

Devuelve el componente corregido completo.

Por qué funciona: los formularios son donde se concentra el daño real. Un error de validación que no se anuncia deja al usuario de lector de pantalla escribiendo a ciegas, pensando que todo ha ido bien.

7. Diseñar los live regions para lectores de pantalla

Prompt:

En esta aplicación hay notificaciones, guardado automático y estados de
carga:

[PEGAR CÓDIGO DE TOASTS / NOTIFICACIONES / ESTADOS DE CARGA]

Necesito que lo revises contra 4.1.3 Mensajes de estado (AA):

1. ¿Qué parte se anuncia con aria-live="polite" (guardado, resultados,
   actualizaciones no urgentes) y qué parte con "assertive" (errores)? No
   uses assertive para nada que no sea urgente: interrumpe todo lo que
   el lector de pantalla esté leyendo.
2. ¿El live region existe ANTES de que se inserte el mensaje? Un contenedor
   que se inserta ya con el texto dentro no se anuncia en todos los lectores
   de pantalla: la región debe estar en el DOM desde el principio.
3. ¿Se está usando el mismo contenedor live para muchos mensajes que se
   reemplazan en menos de un segundo? Eso satura. ¿Hay throttling o cola?
4. ¿El contenido dentro del live region es texto plano? Un toast con un
   botón dentro necesita que el botón sea alcanzable, y eso obliga a
   region's "polite" más un focus manual, no a live region.
5. ¿Los spinners y estados de carga ("Cargando", "Guardando...") se anuncian
   o solo se ven? ¿Cuánto duran? Un "Cargando" que dura 80 ms solo molesta.

Devuelve el patrón corregido con un componente Toast accesible y
reutilizable, en React.

Por qué funciona: casi todo el mundo pone el aria-live en el elemento que aparece, y no funciona. La región tiene que estar ahí antes. Explicar eso al modelo y pedirle el patrón reusable resuelve el problema de raíz de una vez.

8. Adaptar la interfaz a reduced motion

Prompt:

Quiero que mi interfaz respete prefers-reduced-motion.

STACK: [Tailwind 3.4 + framer-motion / CSS puro]
ARCHIVOS CON ANIMACIONES: [PEGAR RUTAS]

Genera:
1. La variante con @media (prefers-reduced-motion: no-preference) y la
   variante con reduce, usando Tailwind motion-safe: y motion-reduce:.
2. Una lista de las animaciones de mi código que NO son esencialmente
   decorativas y necesitan alternativa: paralaje, carouseles, transiciones de
   página, animaciones de entrada, animated charts.
3. Para cada una, la alternativa: cross-fade en vez de slide, contenido
   estático con scroll en vez de carrusel, datos en tabla en vez de gráfico
   animado.
4. El caso especial de los smooth scroll: ¿debe respectarse el flag o
   desactivarse siempre por el motion sickness?
5. Un helper en TypeScript para leer la preferencia del usuario si
   necesito consultarla en JS (por ejemplo para pausar una animación en
   canvas).

Devuelve el código de los archivos modificados.

Por qué funciona: el movimiento no es solo estética; para personas con disorders vestibulares es un problema físico real de nauseas y dolor de cabeza. El tratamiento correcto no es quitar la animación sin más, sino darle una alternativa equivalente en información.

9. Montar la auditoría automática con axe-core y jest-axe

Prompt:

Quiero integrar pruebas de accesibilidad automatizadas en este proyecto:

STACK: [Next.js 15 + React 19 + Vitest / Playwright]
RUTAS A CUBRIR: [PEGAR LISTA]

Escribe:
1. tests/a11y/component.test.tsx con @axe-core/jest + vitest, cubriendo
   los [N] componentes críticos con casos: estado por defecto, estado hover,
   estado disabled, estado de error de formulario, estado vacío, modal
   abierto.
2. tests/a11y/e2e.spec.ts con @axe-core/playwright, recorriendo las rutas
   reales, con waitForNetworkIdle y actúa sobre los overlays antes de
   auditar (porque axe solo ve lo que está en el DOM visible).
3. playwright.config.ts con el proyecto a11y.
4. Un script npm "a11y:report" que ejecute los tests y genere un informe
   HTML con las violaciones agrupadas por criterio WCAG y no solo por
   archivo, que
   es como se lee de verdad una auditoría.
5. Un job en CI que falle solo con violaciones de nivel A y AA (ignora
   "best-practice" de axe, que son sugerencias y no requisitos), con
   tolerancia de 0 violaciones nuevas respecto al baseline.

Nota importante: axe no detecta problemas de foco, de orden del tabulador,
de alt text que "tiene texto pero no dice nada útil" ni de flujos de
pantalla. Dime explícitamente qué queda fuera de esta cobertura
automática.

Por qué funciona: separar violaciones de “best-practice” de las de nivel A y AA es lo que hace que el CI de a11y no se vuelva un campo de minas. Y la nota final sobre los límites es la que evita la falsa sensación de cobertura.

10. Convertir el informe de auditoría en trabajo real

El informe que nadie prioriza no cambia nada.

Prompt:

Tengo este informe de auditoría de accesibilidad:

[PEGAR EL INFORME CON TODOS LOS HALLAZGOS]

Contexto del producto: [QUIÉN LO USA, PARA QUÉ, EN QUÉ ENTORNO - por
ejemplo "panel interno de atención al cliente, uso diario, muchos usuarios
con lectores de pantalla de empresa"]

Quiero convertirlo en un plan de trabajo real:

1. Agrupa los hallazgos por flujo de usuario, no por archivo. Un usuario no
   nota que le faltan tres labels en tres componentes distintos: nota que
   no puede completar el alta.
2. Para cada flujo, lista los hallazgos bloqueantes (impiden completar la
   tarea) separados de los molestos.
3. Estima el esfuerzo (S/M/L) y el riesgo de tocar cada zona, tratando
   aparte lo que toca el flujo de checkout o de pago.
4. Propón el orden: qué se arregla antes por impacto sobre usuarios, qué
   se puede agrupar en una sola PR aunque toque muchos archivos, y qué
   puede esperar al siguiente sprint.
5. Redacta tres tickets de trabajo con criterios de aceptación verificables
   y con el criterio WCAG exacto en la descripción, de forma que otro
   developer pueda cerrarlos sin preguntar nada.

Devuelve un plan de tres sprints con lasdependencias explícitas.

Por qué funciona: la IA es muy buena reagrupando hallazgos en flujos. Convertir “48 problemas” en “3 flujos rotos” es exactamente lo que consigue que un equipo se meta en la solidly.

WCAG 2.2 en una tabla: lo que tienes que mirar

Los nueve criterios nuevos de 2.2 no son un incremento menor. El 2.5.7 (dragging) y el 2.4.11 (focus obscured) han generado más trabajo en equipos que el resto de la revisión anterior junta.

CriterioNivelQué exige en la prácticaCómo se rompe¿Lo ve axe?
2.4.11 Focus Not ObscuredAAEl elemento con foco no puede quedar tapado por contenido flotante (headers sticky, cookies, chat)Un header sticky tapa la opción enfocada de un <select> nativoNo
2.5.7 Dragging MovementsAANinguna acción puede exigir arrastrar; debe haber alternativa con un solo punteroReordenar columnas arrastrando sin botón de moverNo
2.5.8 Target SizeAAMínimo 24×24 px CSS en punterosIconos de 16×16 en la toolbarSí, parcialmente
3.3.6 Accessible AuthenticationAANo se puede exigir memoria o transposición si hay alternativa sin cogniciónPedir memorizar contraseña para un action del bancoNo
3.3.7 Redundant EntryANo repetir la misma información ya dada en el mismo procesoRellenar el email dos veces en un checkoutNo
2.4.13 Focus AppearanceAAAContraste del indicador de foco y área mínimaPersonalizableSí
2.5.3 Label in NameAEl nombre visible debe estar dentro del nombre accesibleBotón “Play” con aria-label="Reproducir vídeo"Sí
3.2.6 Consistent HelpALa ayuda está en el mismo sitio en todas las páginasMover el enlace de contactoNo
3.3.8 Accessible Authentication (Mínimum)AAAutenticación con objeto personal permitidaSinpler, pero con requisito de no UsarFunctionsNo

Un apunte sobre los niveles: el 2.5.8 (target size) parece pequeño y por eso mucha gente lo ignora. En la práctica, las 24×24 px es exactamente lo que separa una app usable en móvil de una frustrante, y los auditores externos lo comprueban siempre.

La tabla también sirve para lo contrario: si una auditoría te pide el nivel AAA, y tu declaración de accesibilidad dice AA, ya tienes una discusión de por medio. Primero decide el nivel, luego mide. Es exactamente el mismo criterio que usaría un cliente al evaluar tu producto, y en eso los tests de evals de las apps con IA se parecen: la métrica importa porque alguien la va a mirar desde fuera.

Anti-patrones en accesibilidad asistida por IA

❌ Confiar en el verdict del modelo

"Aquí tienes tu código. ¿Es accesible?"

→ El modelo dice que sí porque ve labels y alt. No ha ejecutado nada.

❌ ARIA como sustituto del HTML

// ❌ 20 atributos para recrear lo que ya hace la etiqueta
<div role="button" tabIndex={0} onKeyDown={handleEnter}
     aria-label="Enviar mensaje" className="btn-enviar">
  Enviar
</div>
// ✅ Mismo resultado, cero ARIA, foco y teclado gratis
<button type="button" className="btn-enviar">
  Enviar
</button>

❌ aria-label en elementos con texto visible que no lo contiene

// ❌ 2.5.3 Label in Name: el nombre accesible debe empezar por el visible
<button aria-label="Enviar el formulario">Continuar</button>

// ✅
<button aria-label="Enviar el formulario">Continuar →</button>
// o simplemente: <button>Continuar</button>

❌ Live region que se inserta ya con el texto

// ❌ La mayoría de lectores de pantalla no lo anuncia
{error && <div role="alert">{error}</div>}

// ✅ El contenedor existe siempre, el contenido cambia dentro
<div role="alert" aria-live="assertive">{error}</div>

❌ Placeholder como etiqueta

// ❌ Desaparece al escribir, no es accesible, no sobrevive al zoom
<input placeholder="Email" />

// ✅
<label htmlFor="email">Email</label>
<input id="email" type="email" autoComplete="email" />

❌ Desactivar el outline

/* ❌ El peor commit de accesibilidad de la historia */
button:focus { outline: none; }

/* ✅ Visible siempre, y coherente en todos los componentes */
button:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 2px;
}

Errores frecuentes y cómo se resuelven

“axe dice que tengo 0 violaciones, pero un auditor me da 40” Esperado y documentado: axe cubre entre el 20% y el 40% de los problemas WCAG. Si tu auditoría automática sale limpia, no tienes ni un problema: tienes un 30% de cobertura. Los dos informes miden cosas distintas.

El equipo pide auditorías con herramientas de pago (Siteimprove, Deque) y aun así no mejora Porque esas herramientas son, en el fondo, el mismo análisis estático con más reglas. El salto de calidad de verdad viene de las pruebas manuales con teclado y lector de pantalla, que es donde se Sfinge el 70% restante.

Los problemas aparecen “solo en Safari” o “solo en móvil” Causa habitual: estilos de foco que dependen del :focus-visible y no están redefinidos, o 100vh en móviles que tapa el elemento enfocado con la barra de direcciones. Prueba siempre en el navegador real antes de cerrar el hallazgo.

El componente <select> nativo falla la auditoría y el equipo quiere sustituirlo por un combobox custom Casi siempre es mala idea. El select nativo es accesible por definición, funciona en móvil y con lector de pantalla. Si el diseño no encaja, el problema es el diseño, no el componente. Si aun así se sustituye, hay que cubrirlo con el prompt 3 entero.

“El modal pasa todas las pruebas pero luego el foco desaparece” El foco no se pierde porque falte un atributo: se pierde porque la lógica de restoration usa un id que ya no existe tras la navegación. Necesitas un stack de focus trap con restoration robust, y un test de integración que navegue y vuelva.

La auditoría se hace una vez y nunca se repite La accesibilidad es una propiedad del código, no una revisión puntual. Conéctala a la CI (prompt 9) y acepta el 0-tolerancia para A y AA. Es lo único que se sostiene en el tiempo.

El flujo completo de una auditoría que sirve

┌────────────────────────────────────────────────────────┐
│  1. RECORRIDO MANUAL con teclado (20 min)               │
│     Tab, Shift+Tab, Escape, foco visible                │
│     ──► notas crudas, capturas de focus perdido         │
└────────────────────┬───────────────────────────────────┘
                     ▼
┌────────────────────────────────────────────────────────┐
│  2. AUTOMÁTICO  axe-core + Lighthouse + PAW            │
│     ──► 30% de los hallazgos, objetivos y repetibles   │
└────────────────────┬───────────────────────────────────┘
                     ▼
┌────────────────────────────────────────────────────────┐
│  3. IA prompts 1-8 con el código y las notas (prompt 10)│
│     ──► hallazgos por flujo, con criterio WCAG exacto   │
└────────────────────┬───────────────────────────────────┘
                     ▼
┌────────────────────────────────────────────────────────┐
│  4. TRIAGE: agrupar por flujo, no por archivo (prompt 10)│
│     ──► 3 flujos rotos en vez de 48 problemas sueltos   │
└────────────────────┬───────────────────────────────────┘
                     ▼
┌────────────────────────────────────────────────────────┐
│  5. CI con axe + jest-axe (prompt 9)                    │
│     ──► 0 tolerancia en A y AA, baseline para el resto │
└────────────────────────────────────────────────────────┘

El paso 1 es el que más valor da y el que todo el mundo se salta. Veinte minutos de tabulador te dan material que ningún análisis estático te va a dar nunca.

Conclusión

La accesibilidad no es un trámite que le pasas a un linter. Es una propiedad del producto que la auditoría automática solo puede intuir en un tercio de los casos, y la IA es muy buena cubriendo el otro tercio razonando, no ejecutando.

Lo que hacen estos prompts es codificar el razonamiento: qué preguntarle al modelo, con qué contexto y en qué orden. El modelo no va a ver que tu modal pierde el foco, pero sí sabe decirte que un modal debe devolver el foco al elemento que lo abrió, y a partir de ahí tú verificas.

El orden importa: primero el recorrido manual, después el automatizado, después la IA. Al revés pierdes el tiempo. Y al final, todo a la CI, porque una auditoría que depende de que alguien se acuerde de hacerla en el trimestre no arregla nada.

Si quieres el generador para estructurar estos prompts antes de pegarlos en tu herramienta, el generador de prompts te ahorra el JSON.

Y si lo que quieres es construir la base de componentes ya accesibles antes de tener que auditarlos, empieza por la guía de shadcn/ui. shadcn copiar los fuentes es la excepción a la regla del componente genérico: te llevas el código y puedes arreglarle los problemas de foco directamente.

La accesibilidad no te va a dar usuarios. Te va a quitar la lista de motivos para decir que no. Y en 2026, esa lista es larga.

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.