Auditoría de accesibilidadque pulsa Tab.
Recorre tu web con el teclado, mide el foco píxel a píxel y te da cada arreglo en código.
- Fases
- 5 de 5Todas completas
- Foco visible
- 1 de 2011 débiles, 8 invisibles
- Por severidad
- 15Crítica 3, alta 8, media 3, baja 1
El informe, hallazgo a hallazgo.
Cada hallazgo dice de qué fase sale, qué criterio WCAG incumple, en qué elemento está y cómo se arregla.
Teclado: trampa de teclado. Visión: hecha.
axe-coreZoom y espaciadoTecladoVisiónOrden del tabulador

Garantiza que los elementos <img> tienen texto alternativo o un rol de none o presentation
img[src$="bolsa-etiopia.svg"]Diagnóstico completo de axe
Corregir cualquiera de las siguientes incidencias: El elemento no tiene un atributo alt El atributo aria-label no existe o está vacío El atributo aria-labelledby no existe, hace referencia a elementos inexistentes o hace referencia a elementos vacíos El elemento no tiene atributo title o el atributo title está vacío Element's default semantics were not overridden with role="none" or role="presentation"

Alt propuesto tras mirar la imagen: «Bolsa de café Etiopía Guji lavado de Tostadero Norte con granos de café alrededor». La imagen incluye texto informativo sobre el origen y variedad del café (Etiopía Guji lavado) que no aparece en el texto adyacente.
index.html, propuesto por el modelo eliminado: <img src="/demo/img/bolsa-etiopia.svg" width="560" height="400">añadido: <img src="/demo/img/bolsa-etiopia.svg" width="560" height="400" alt="Bolsa de café Etiopía Guji lavado de Tostadero Norte con granos de café alrededor">
El foco gira entre 2 paradas y ni Tab, ni Shift+Tab, ni Escape lo sacan. Quien navega con teclado queda atrapado.
#suscripcion > input
Sin foco 
Con foco (Tab) <input type="email" aria-label="Correo electrónico" placeholder="tu@correo.com" style="">#suscripcion > button
Sin foco 
Con foco (Tab) <button type="button">Quiero el descuento</button>
Garantiza que el contraste entre colores de primer plano y fondo cumple los límites de la ratio para contraste WCAG 2 AA
article:nth-child(1) > .notesDiagnóstico completo de axe
Corregir cualquiera de las siguientes incidencias: El elemento tiene un contraste de color insuficiente de 2.3 (color de primer plano: #b3a898, color de fondo: #fffdf9, tamaño de fuente: 11.3pt (15px), grosor de fuente: normal). Ratio de contraste esperado: 4.5:1

Sube el contraste de 2.30:1 a 4.50:1 (objetivo 4.5:1) cambiando solo la luminosidad: el tono se conserva.
styles.css, arreglo calculado article:nth-child(1) > .notes {eliminado: color: #b3a898;añadido: color: #7f7465;}article:nth-child(2) > .notesDiagnóstico completo de axe
Corregir cualquiera de las siguientes incidencias: El elemento tiene un contraste de color insuficiente de 2.3 (color de primer plano: #b3a898, color de fondo: #fffdf9, tamaño de fuente: 11.3pt (15px), grosor de fuente: normal). Ratio de contraste esperado: 4.5:1

Sube el contraste de 2.30:1 a 4.50:1 (objetivo 4.5:1) cambiando solo la luminosidad: el tono se conserva.
styles.css, arreglo calculado article:nth-child(2) > .notes {eliminado: color: #b3a898;añadido: color: #7f7465;}article:nth-child(3) > .notesDiagnóstico completo de axe
Corregir cualquiera de las siguientes incidencias: El elemento tiene un contraste de color insuficiente de 2.3 (color de primer plano: #b3a898, color de fondo: #fffdf9, tamaño de fuente: 11.3pt (15px), grosor de fuente: normal). Ratio de contraste esperado: 4.5:1

Sube el contraste de 2.30:1 a 4.50:1 (objetivo 4.5:1) cambiando solo la luminosidad: el tono se conserva.
styles.css, arreglo calculado article:nth-child(3) > .notes {eliminado: color: #b3a898;añadido: color: #7f7465;}
Garantiza que cada documento HTML tiene un atributo lang
htmlCorregir cualquiera de las siguientes incidencias: El elemento <html> no tiene un atributo lang
Se añade el atributo lang con valor "es" al elemento html.
index.html, propuesto por el modelo eliminado: <html>añadido: <html lang="es">
Con un zoom del 400 % (o en un móvil estrecho), estos bloques no se adaptan al ancho y obligan a leer desplazándose en dos direcciones. Quien amplía la página pierde el hilo de cada línea.
headerA 320 px, 4 elementos de este bloque llegan hasta 829 px.

Deja que la fila se parta en varias líneas cuando no cabe.
styles.css, arreglo calculado añadido: @media (max-width: 40rem) {añadido: header {añadido: flex-wrap: wrap;añadido: }añadido: }#cafesA 320 px, 2 elementos de este bloque llegan hasta 804 px.

Cambia las columnas fijas por una rejilla fluida: tantas columnas como quepan, y una sola en pantallas estrechas.
styles.css, arreglo calculado añadido: @media (max-width: 40rem) {añadido: #cafes {añadido: grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));añadido: }añadido: }#suscripcionA 320 px, 2 elementos de este bloque llegan hasta 459 px.

Deja que la fila se parta en varias líneas cuando no cabe.
styles.css, arreglo calculado añadido: @media (max-width: 40rem) {añadido: #suscripcion {añadido: flex-wrap: wrap;añadido: }añadido: }section.hero:nth-of-type(1)A 320 px, un elemento de este bloque llega hasta 411 px.

Cambia las columnas fijas por una rejilla fluida: tantas columnas como quepan, y una sola en pantallas estrechas.
styles.css, arreglo calculado añadido: @media (max-width: 40rem) {añadido: section.hero:nth-of-type(1) {añadido: grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));añadido: }añadido: }footerA 320 px, un elemento de este bloque llega hasta 382 px.
Deja que la fila se parta en varias líneas cuando no cabe.
styles.css, arreglo calculado añadido: @media (max-width: 40rem) {añadido: footer {añadido: flex-wrap: wrap;añadido: }añadido: }
Al recibir el foco con Tab, estos elementos no cambian ni un píxel perceptible respecto al estado sin foco. Quien usa teclado no sabe dónde está.
nav > a:nth-of-type(1)Parada 2: 0 píxeles de indicador medidos.

Sin foco 
Con foco (Tab) Añade un indicador de foco de dos colores (técnica C40 de WCAG): un anillo oscuro con un borde claro por dentro, que contrasta con cualquier fondo.
styles.css, arreglo calculado añadido: nav > a:nth-of-type(1):focus-visible {añadido: outline: 3px solid #0B0C0C;añadido: outline-offset: 2px;añadido: box-shadow: 0 0 0 2px #FFFFFF;añadido: }nav > a:nth-of-type(2)Parada 3: 0 píxeles de indicador medidos.

Sin foco 
Con foco (Tab) Añade un indicador de foco de dos colores (técnica C40 de WCAG): un anillo oscuro con un borde claro por dentro, que contrasta con cualquier fondo.
styles.css, arreglo calculado añadido: nav > a:nth-of-type(2):focus-visible {añadido: outline: 3px solid #0B0C0C;añadido: outline-offset: 2px;añadido: box-shadow: 0 0 0 2px #FFFFFF;añadido: }nav > a:nth-of-type(3)Parada 4: 0 píxeles de indicador medidos.

Sin foco 
Con foco (Tab) Añade un indicador de foco de dos colores (técnica C40 de WCAG): un anillo oscuro con un borde claro por dentro, que contrasta con cualquier fondo.
styles.css, arreglo calculado añadido: nav > a:nth-of-type(3):focus-visible {añadido: outline: 3px solid #0B0C0C;añadido: outline-offset: 2px;añadido: box-shadow: 0 0 0 2px #FFFFFF;añadido: }nav > a:nth-of-type(4)Parada 5: 0 píxeles de indicador medidos.

Sin foco 
Con foco (Tab) Añade un indicador de foco de dos colores (técnica C40 de WCAG): un anillo oscuro con un borde claro por dentro, que contrasta con cualquier fondo.
styles.css, arreglo calculado añadido: nav > a:nth-of-type(4):focus-visible {añadido: outline: 3px solid #0B0C0C;añadido: outline-offset: 2px;añadido: box-shadow: 0 0 0 2px #FFFFFF;añadido: }
Con el foco puesto, otro contenido (una cabecera fija, un banner…) cubre por completo el elemento.
footer > a:nth-of-type(1)Parada 15.

Sin foco 
Con foco (Tab) Reserva el espacio de las capas fijas al hacer scroll hacia un elemento con foco.
styles.css, arreglo calculado añadido: html {añadido: scroll-padding-block: var(--sticky-header-height, 5rem) 2rem;añadido: }footer > a:nth-of-type(2)Parada 16.

Sin foco 
Con foco (Tab) Reserva el espacio de las capas fijas al hacer scroll hacia un elemento con foco.
styles.css, arreglo calculado añadido: html {añadido: scroll-padding-block: var(--sticky-header-height, 5rem) 2rem;añadido: }footer > a:nth-of-type(3)Parada 17.

Sin foco 
Con foco (Tab) Reserva el espacio de las capas fijas al hacer scroll hacia un elemento con foco.
styles.css, arreglo calculado añadido: html {añadido: scroll-padding-block: var(--sticky-header-height, 5rem) 2rem;añadido: }footer > a:nth-of-type(4)Parada 18.

Sin foco 
Con foco (Tab) Reserva el espacio de las capas fijas al hacer scroll hacia un elemento con foco.
styles.css, arreglo calculado añadido: html {añadido: scroll-padding-block: var(--sticky-header-height, 5rem) 2rem;añadido: }
Se pulsó Enter (o Espacio) sobre estos controles no nativos y no cambió nada: ni el DOM, ni el foco, ni se intentó navegar. Seguramente solo escuchan el ratón.
article.product:nth-of-type(1) > div.row > div.addParada 10, rol «button».

Sin foco 
Con foco (Tab) Usa un <button> nativo: responde a Enter y a Espacio sin JavaScript extra.
index.html, arreglo calculado eliminado: <div class="add" role="button" tabindex="0">Añadir</div>añadido: <button type="button" class="add">Añadir</button>article.product:nth-of-type(2) > div.row > div.addParada 12, rol «button».

Sin foco 
Con foco (Tab) Usa un <button> nativo: responde a Enter y a Espacio sin JavaScript extra.
index.html, arreglo calculado eliminado: <div class="add" role="button" tabindex="0">Añadir</div>añadido: <button type="button" class="add">Añadir</button>article.product:nth-of-type(3) > div.row > div.addParada 14, rol «button».

Sin foco 
Con foco (Tab) Usa un <button> nativo: responde a Enter y a Espacio sin JavaScript extra.
index.html, arreglo calculado eliminado: <div class="add" role="button" tabindex="0">Añadir</div>añadido: <button type="button" class="add">Añadir</button>
La medición detecta un cambio pequeño y el modelo de visión, comparando los dos estados, concluye que una persona no lo percibiría.
Confianza del modelo: 95 %
article.product:nth-of-type(1) > div.row > a.detailsDiagnóstico completo de axe
Parada 9: 0 px de indicador medidos, de 399 px exigidos por 2.4.13. El borde que rodea el enlace en la imagen B es extremadamente tenue yapenas se distingue del fondo blanco, resultando imperceptible a simple vista.

Sin foco 
Con foco (Tab) Añade un indicador de foco de dos colores (técnica C40 de WCAG): un anillo oscuro con un borde claro por dentro, que contrasta con cualquier fondo.
styles.css, arreglo calculado añadido: article.product:nth-of-type(1) > div.row > a.details:focus-visible {añadido: outline: 3px solid #0B0C0C;añadido: outline-offset: 2px;añadido: box-shadow: 0 0 0 2px #FFFFFF;añadido: }article.product:nth-of-type(2) > div.row > a.detailsDiagnóstico completo de axe
Parada 11: 0 px de indicador medidos, de 399 px exigidos por 2.4.13. El recuadro del foco en la imagen B es tan suave y tenue que no es perceptible claramente a una distancia normal de lectura.

Sin foco 
Con foco (Tab) Añade un indicador de foco de dos colores (técnica C40 de WCAG): un anillo oscuro con un borde claro por dentro, que contrasta con cualquier fondo.
styles.css, arreglo calculado añadido: article.product:nth-of-type(2) > div.row > a.details:focus-visible {añadido: outline: 3px solid #0B0C0C;añadido: outline-offset: 2px;añadido: box-shadow: 0 0 0 2px #FFFFFF;añadido: }article.product:nth-of-type(3) > div.row > a.detailsDiagnóstico completo de axe
Parada 13: 0 px de indicador medidos, de 399 px exigidos por 2.4.13. El fino contorno alrededor del enlace en la imagen B presenta un contraste tan bajo que resulta imperceptible a simple vista.

Sin foco 
Con foco (Tab) Añade un indicador de foco de dos colores (técnica C40 de WCAG): un anillo oscuro con un borde claro por dentro, que contrasta con cualquier fondo.
styles.css, arreglo calculado añadido: article.product:nth-of-type(3) > div.row > a.details:focus-visible {añadido: outline: 3px solid #0B0C0C;añadido: outline-offset: 2px;añadido: box-shadow: 0 0 0 2px #FFFFFF;añadido: }
El modelo de visión ha comparado la imagen con su alt y el contexto de la página: el alt no transmite lo que la imagen comunica.
Confianza del modelo: 98 %
article.product:nth-of-type(1) > imgAlt actual: «IMG_0231_final_v2.svg». El texto alternativo actual es el nombre de un archivo en lugar de describir el contenido visual de la imagen.

Alt propuesto: «Taza de café humeante sobre un plato».
index.html, propuesto por el modelo eliminado: <img src="/demo/img/taza.svg" alt="IMG_0231_final_v2.svg" width="360" height="260">añadido: <img src="/demo/img/taza.svg" alt="Taza de café humeante sobre un plato" width="360" height="260">
Garantiza que todo el contenido de la página está incluido en puntos de referencia
strongCorregir cualquiera de las siguientes incidencias: La página tiene contenido no incluido en puntos de referencia

<strong>−10 % en tu primera suscripción</strong>input[type="email"]Corregir cualquiera de las siguientes incidencias: La página tiene contenido no incluido en puntos de referencia

<input type="email" aria-label="Correo electrónico" placeholder="tu@correo.com" style="">
Con el interlineado, el espacio entre letras y el espacio entre palabras que exige WCAG 1.4.12 (los que aplican quienes leen con dislexia o baja visión), parte del texto deja de verse porque su caja tiene un alto o un ancho fijos y oculta lo que sobra.
span.sello«Tueste claro · Lote 12»: se dejan de ver 7 px a lo ancho.

Que la caja crezca con su texto en lugar de recortarlo: sin alto ni ancho fijos, con un mínimo para conservar el diseño.
styles.css, arreglo calculado añadido: span.sello {añadido: width: auto;añadido: max-width: 100%;añadido: white-space: normal;añadido: }
El modelo de visión ha comparado la imagen con su alt y el contexto de la página: el alt no transmite lo que la imagen comunica.
Confianza del modelo: 95 %
article.product:nth-of-type(3) > imgAlt actual: «Cafetera». El texto actual es excesivamente genérico y omite que se trata de una cafetera de goteo tipo Chemex con café preparado en su base.

Alt propuesto: «Cafetera de filtro tipo Chemex con café preparado».
index.html, propuesto por el modelo eliminado: <img src="/demo/img/cafetera.svg" alt="Cafetera" width="360" height="260">añadido: <img src="/demo/img/cafetera.svg" alt="Cafetera de filtro tipo Chemex con café preparado" width="360" height="260">
El modelo de visión ve una imagen puramente decorativa con un alt descriptivo: los lectores de pantalla la leen sin aportar nada. Debería llevar alt="".
Confianza del modelo: 98 %
#metodos > imgDiagnóstico completo de axe
Alt actual: «Ramas de cafeto con cerezas rojas y hojas verdes dibujadas en línea». La imagen actúa como un elemento gráfico puramente ornamental o separador visual que no aporta información relevante al contenido, por lo que debe ser ignorada por los lectores de pantalla usando alt="".

Marca la imagen como decorativa con alt="".
index.html, propuesto por el modelo eliminado: <img src="/demo/img/ornamento.svg" alt="Ramas de cafeto con cerezas rojas y hojas verdes dibujadas en línea" width="360" height="120">añadido: <img src="/demo/img/ornamento.svg" alt="" width="360" height="120">
Primero se mide. Después, el modelo.
Tres lentes sobre la misma página. El tamaño de cada una es lo que tardó en la auditoría de muestra.
Carga de la página: 1,1 s. Reflujo a 320 px y espaciado de texto: 0,9 s. Total: 108,1 s, con la visión en paralelo al teclado. Aparte, 89,6 s esperando la cuota gratuita de Gemini.
Un agente que pulsa Tab
Recorre la página con pulsaciones reales y compara cada elemento con foco y sin foco, píxel a píxel. Si no cambia nada, el foco es invisible. Si el foco no puede salir de un grupo, es una trampa.
WCAG 2.1.1, 2.1.2, 2.4.7, 2.4.11 y 2.4.13.


0 px²de indicador
Resultado en la demo: 0 píxeles de indicador en los cuatro enlaces del menú (el primero necesitaba 236 px²), y una trampa de teclado entre 2 elementos de la franja promocional.
El Acta Europea de Accesibilidad ya se aplica.
Si vendes en línea en la UE, tu web debe cumplir WCAG 2.1 AA. Una auditoría automática no lo certifica, pero te dice por dónde empezar.
2019
La UE aprueba el Acta Europea de Accesibilidad
Directiva (UE) 2019/882: requisitos comunes de accesibilidad para productos y servicios.
2023
España la transpone
Con la Ley 11/2023, de 8 de mayo.
28 de junio de 2025
Se aplicaEn vigor
Desde ese día, los servicios que se lanzan o se renuevan tienen que cumplirla.
28 de junio de 2030
Termina el margen
Lo que ya se usaba para prestar el servicio antes de junio de 2025 puede seguir usándose hasta entonces.
- A quién afecta
- Comercio electrónico, banca para consumidores, transporte de viajeros, comunicaciones electrónicas y libros electrónicos, entre otros.
- Con qué se mide
- Con la norma EN 301 549, que remite a WCAG 2.1 AA. LupA11y audita WCAG 2.2 AA, que la incluye.
- Qué se arriesga
- Multas de 301 € a 1.000.000 € según la gravedad (RDL 1/2013). Leves hasta 30.000 € y graves hasta 90.000 €.
- Quién queda fuera
- Las microempresas que prestan servicios: menos de 10 personas y hasta 2 M€ de facturación o balance.
Ley 11/2023 en el BOE (se abre en otra pestaña)Directiva (UE) 2019/882 (se abre en otra pestaña)Información orientativa, no asesoramiento legal.
Tu CI y tu editor, el mismo motor.
La web, la GitHub Action y el servidor MCP llaman al mismo paquete y devuelven el mismo contrato Zod.
- WebEsta página y su API de streaming.
- GitHub ActionUn check en cada pull request.
- Servidor MCPClaude Code, Cursor y otros clientes.
- CLILa terminal y cualquier otra CI.
Cada pull request se audita antes de fusionarse. Si aparece algo de severidad alta, el check falla y el resumen queda en la pestaña del job.
- Dónde corre
- En el propio job, contra la app levantada en localhost: una URL, varias o las de un sitemap.
- Qué deja
- El resumen con los diffs en $GITHUB_STEP_SUMMARY, anotaciones, un comentario en el PR y SARIF para la pestaña de seguridad.
- Cuándo falla
- Por encima del umbral que elijas. Con una línea base, solo por lo que es nuevo o empeora.
- Sin Gemini
- Sigue funcionando: la visión se marca como omitida.
name: Accesibilidad
on: [pull_request]
jobs:
lupa11y:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm ci && npm run build
- run: npm start &
- run: npx wait-on http://localhost:3000
- uses: Adriiiii24/LupA11y@v0
with:
url: http://localhost:3000
fail-on: high
sarif-path: lupa11y.sarif
comment-pr: true
gemini-api-key: ${{ secrets.GEMINI_API_KEY }}
- uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: lupa11y.sarifEvidencia, no promesas.
Ninguna herramienta automática certifica WCAG. LupA11y te da el mapa y los arreglos; el orden de lectura y los contenidos complejos los revisa una persona.
Auditar una URL