Tag: Accesibilidad

  • Starlight 0.42: menos JavaScript, menú móvil roto en silencio

    Starlight 0.42: menos JavaScript, menú móvil roto en silencio

    El viernes actualicé la documentación de un proyecto interno a Starlight 0.42. npx @astrojs/upgrade, build en verde, Lighthouse un punto mejor. Cerré el portátil.

    El lunes un compañero me pasa una captura desde el móvil. El menú abría perfecto. Pero el botón se quedaba gris. Nuestro color de marca al abrir el menú había desaparecido.

    Nada había fallado. Ese es el problema. El CSS no lanza errores: cuando un selector deja de coincidir con algo, se calla y sigue. El build no te avisa, los tests no lo ven y tú te enteras cuando alguien abre el sitio en un teléfono.

    El anuncio oficial de la release vende una paradoja simpática —menos JavaScript y más JavaScript a la vez— y para los detalles te remite al CHANGELOG. Este post es el CHANGELOG explicado, con el CSS concreto que tienes que cambiar.

    El titular es "menos JavaScript". La letra pequeña es que si tocaste el menú móvil con CSS o JS propio, la 0.42 te lo rompe sin decir nada.


    La paradoja de Starlight 0.42: "un 100 % más de JavaScript"

    El chiste es del propio anuncio y tiene truco. Hay dos JavaScript distintos aquí y el post oficial los mezcla a propósito.

    Uno es el JavaScript que va dentro del paquete de npm. Ese sube: Starlight ha dejado de publicar TypeScript y ahora distribuye JavaScript compilado.

    El otro es el JavaScript que llega al navegador de quien lee tu documentación. Ese baja: el menú móvil ya no necesita JS para funcionar.

    Build-time contra runtime. Son cosas distintas y confundirlas es el error clásico al leer notas de release. Con la 0.42 en la mano, un sitio Starlight medio sigue enviando entre 3 y 13 veces menos JavaScript al usuario final que herramientas comparables: MkDocs, Sphinx, VitePress, Nextra, GitBook o Docusaurus.

    Es la misma lógica que ya movía a Astro con las server islands: decidir con precisión qué se ejecuta en el servidor y qué viaja al cliente, en vez de mandarlo todo y confiar. Astro lleva varias releases insistiendo en lo mismo, desde que server islands y actions se estabilizaron en la 6.2.


    Por qué Starlight publicaba TypeScript y por qué ha parado

    Hay una rareza de los proyectos Astro que mucha gente no conoce: puedes publicar archivos .ts directamente en npm y funcionan cuando alguien los instala. Sin build step. Genial para prototipar rápido, y así se venía publicando Starlight desde el principio.

    El coste aparecía en tu proyecto, no en el suyo.

    Si haces typecheck con tsc, TypeScript revisa cualquier .ts que encuentre importado, incluso dentro de node_modules. Así que acababas comprobando el código fuente de Starlight. Y no con la configuración del paquete, sino con la tuya. En algunos casos el rendimiento de tipos en el editor también se resentía. En una base de código del tamaño de Starlight, esos problemas se acumulan.

    La 0.42 compila sus fuentes y distribuye JavaScript más archivos de declaración (.d.ts). El resultado esperable es un typecheck más rápido en tu máquina — y más aún cuando aterrice el compilador de TypeScript escrito en Go.

    Detalle que conviene tener claro: un .d.ts describe tipos en tiempo de compilación y desaparece en tiempo de ejecución. No valida nada.

    La validación de verdad ya la estás usando aunque no te hayas fijado: el frontmatter de tus páginas lo comprueba Starlight con esquemas de Zod durante el build. Y si tu sitio además tiene formularios o endpoints, ahí hace falta el mismo tipo de esquema pero ejecutándose en runtime. Tipos y validación resuelven problemas distintos, aunque la gente los meta en el mismo saco.


    La Popover API: por qué el menú móvil deja de necesitar tu JavaScript

    Aquí está el cambio de fondo.

    Antes, el menú móvil era una máquina de estados escrita a mano: un custom element <starlight-menu-button> que escuchaba clics, cambiaba aria-expanded en el botón y ponía un atributo en el <body>. Si el JavaScript no llegaba a ejecutarse, no había menú.

    Y hay más razones de las que la gente cree para que el JavaScript no se ejecute. La red se cae a medias. Otro script peta antes y se lleva por delante el resto del bundle. Una extensión del navegador se mete por medio. Alguien lo tiene desactivado. En un sitio de documentación —que muchas veces es la primera vez que alguien te ve— eso es una puerta cerrada.

    La 0.42 delega ese estado en la Popover API del navegador. El menú abre aunque tu JavaScript nunca llegue.

    Menos código propio, más primitiva nativa. Es la dirección correcta.

    Pero ojo con la consecuencia: si el estado ya no vive en un atributo del botón, tu CSS no tiene a qué agarrarse.


    Los 3 breaking changes de Starlight 0.42, en una tabla

    Starlight 0.42 rompe tres cosas y solo tres. Esta es la traducción directa de código viejo a código nuevo:

    Qué desaparece Reemplazo en 0.42 A quién afecta
    <starlight-menu-button> y aria-expanded .sl-menu-button y .sl-menu-button:has(~ :popover-open) Estilos, temas o component overrides que apuntaban al botón del menú móvil
    body[data-mobile-menu-expanded] body:has(sl-sidebar-pane:popover-open) CSS o JS propio que reaccionaba a la apertura del menú
    Opción tagline en astro.config Sin reemplazo: se borra Configs que la declararon (nunca hizo nada)

    Si ninguna de las tres filas aparece en tu código, la actualización es transparente. Si aparece alguna, las tres secciones siguientes son tuyas.


    Breaking change 1: el botón del menú móvil

    El botón ya no va envuelto en el custom element <starlight-menu-button> y ya no usa aria-expanded. Ahora apuntas al botón con la clase .sl-menu-button y lees el estado abierto con la pseudo-clase :popover-open.

    Así estaba tu CSS antes:

    /* Antes: Starlight 0.41 y anteriores */
    starlight-menu-button button {
      border-radius: 999px;
      background: var(--sl-color-gray-6);
    }
    
    starlight-menu-button[aria-expanded="true"] button {
      background: var(--dc-brand);
      color: var(--sl-color-white);
    }
    

    Y así queda en la 0.42:

    /* Después: Starlight 0.42 */
    .sl-menu-button {
      border-radius: 999px;
      background: var(--sl-color-gray-6);
    }
    
    /* El estado abierto lo expone el navegador en el panel, no en el botón.
       Seleccionas el botón que tiene un popover abierto como hermano. */
    .sl-menu-button:has(~ :popover-open) {
      background: var(--dc-brand);
      color: var(--sl-color-white);
    }
    

    Fíjate en lo que ha cambiado de verdad. No es un nombre de clase: es de quién es el estado.

    Antes el estado estaba en el botón, porque lo escribía JavaScript. Ahora está en el panel, porque lo gestiona el navegador. Por eso el selector nuevo tiene que ser relacional: :has() con ~ :popover-open significa "este botón, cuando un hermano posterior suyo está abierto".


    Breaking change 2: adiós a data-mobile-menu-expanded

    El atributo data-mobile-menu-expanded que Starlight añadía al <body> mientras el menú estaba abierto ya no existe. Si lo usabas para ocultar una barra flotante, bloquear el scroll o apagar una animación, ese bloque de CSS ha dejado de aplicarse.

    /* Antes */
    body[data-mobile-menu-expanded] .dc-cta-flotante {
      display: none;
    }
    
    /* Después */
    body:has(sl-sidebar-pane:popover-open) .dc-cta-flotante {
      display: none;
    }
    

    Si además tenías JavaScript propio reaccionando al menú, el cambio te ahorra código. Antes tocaba vigilar un atributo:

    // Antes: espiar el atributo que ponía Starlight
    const boton = document.querySelector('starlight-menu-button button');
    
    new MutationObserver(() => {
      const abierto = boton.getAttribute('aria-expanded') === 'true';
      document.body.classList.toggle('menu-abierto', abierto);
    }).observe(boton, { attributeFilter: ['aria-expanded'] });
    

    Ahora el navegador te lo cuenta él solo con un evento nativo:

    // Después: el panel es el popover y emite un evento toggle
    const panel = document.querySelector('sl-sidebar-pane[popover]');
    
    panel?.addEventListener('toggle', (event) => {
      const abierto = event.newState === 'open';
      document.body.classList.toggle('menu-abierto', abierto);
    });
    

    Un MutationObserver menos en tu sitio. Esa es la parte buena de apoyarse en primitivas del navegador: el código que borras no puede fallar.


    Breaking change 3: la opción tagline ya no existe

    Se elimina de la configuración. Nunca se llegó a usar para nada, así que no hay reemplazo: la borras de tu astro.config y listo. Si no la quitas, la validación de config te lo dirá.


    Requisitos mínimos y navegadores que se caen

    Starlight 0.42 exige Astro v7.2.10 o superior. Si usas @astrojs/markdown-satteri, necesitas 0.4.0 o superior. Si sigues con @astrojs/markdown-remark, 7.3.0 o superior.

    Si tu sitio todavía está en Astro v6, el salto grande no es este release, es el anterior: repasa primero las novedades de Astro v7 y hazlo en un PR aparte. Actualizar dos majors en el mismo commit es la forma más rápida de perder la tarde.

    Y hay una lista de navegadores que dejan de tener soporte oficial:

    Navegador Versión mínima soportada
    Chromium 116 (agosto de 2023)
    Safari 17.0 (septiembre de 2023)
    Firefox 125 (abril de 2024)

    No es un capricho, pero tampoco es el mínimo exacto de la API. Firefox 125 y Safari 17.0 son justo las versiones donde aterrizó la Popover API. En Chromium llegó antes, en la 114, así que ahí Starlight se ha guardado dos versiones de margen. Y en iPhone no hay sorpresa: el Safari de iOS la soporta desde la misma 17.0 que el de escritorio.

    El precio de apoyarse en el navegador es aceptar su calendario. Mira tus analíticas antes de decidir: en un sitio de documentación técnica, ese tráfico suele redondear a cero.


    Lo que mejora sin que hagas nada

    Dos cosas llegan gratis con la actualización.

    Rendimiento. La última versión de Sätteri —el motor de Markdown y MDX de Astro escrito en Rust, que llegó en Astro 6.4 y es el motor por defecto desde Astro 7— les ha permitido optimizar componentes y plugins de Markdown, y reducir el número de dependencias de Starlight. El procesado de datos del sidebar es ahora hasta 1.400 veces más rápido, y eso se nota de verdad en sitios con sidebars grandes o muy anidados.

    Ojo con un detalle que no es opcional. Sätteri no ejecuta plugins de remark ni de rehype: tiene su propio sistema de plugins mdast y hast, y no hay fallback automático.

    Y aquí no eliges tú. Como es el motor por defecto de Astro 7, y la 0.42 exige Astro v7.2.10 o superior, la actualización te lo puede cambiar sola. Si tu pipeline depende de remark o rehype, tienes que quedarte explícitamente en @astrojs/markdown-remark 7.3.0 o superior. Compruébalo antes de lanzar el upgrade, no después.

    Accesibilidad. El menú móvil atrapa el foco mientras está abierto, para que no puedas tabular hacia la página que queda escondida debajo. Detrás está el criterio de éxito WCAG 2.4.11, «Focus Not Obscured (Minimum)»: el elemento con el foco no puede quedar tapado por lo que hay encima. Se agradece en viewports pequeños o con mucho zoom. Llegó en la 0.41.3, así que si vienes de la última 0.41.x ya lo tienes. Si esto lo habías parcheado tú a mano, bórralo: ahora hay dos implementaciones peleándose por el foco.


    Cómo actualizar a Starlight 0.42 paso a paso

    1. Comprueba que estás en Astro v7. Starlight 0.42 exige Astro v7.2.10 o superior. Si vienes de Astro v6, haz ese salto antes y en un PR aparte.

    2. Busca lo que se va a romper. Cuatro grep antes de tocar nada:

      grep -rn "starlight-menu-button" src/
      grep -rn "data-mobile-menu-expanded" src/
      grep -rn "aria-expanded" src/styles/
      grep -rn "tagline" astro.config.*
      

      Cada resultado es una línea que hay que migrar. Si no aparece nada, actualiza tranquilo.

    3. Actualiza. Un solo comando sube Starlight, Astro y el resto de integraciones a la vez:

      npx @astrojs/upgrade
      
    4. Migra el CSS y el JS con la tabla de equivalencias de más arriba: .sl-menu-button, .sl-menu-button:has(~ :popover-open) y body:has(sl-sidebar-pane:popover-open).

    5. Abre el sitio en un móvil de verdad y toca el botón del menú. No en el simulador de Chrome: en un teléfono. Es el único sitio donde estos tres breaking changes se manifiestan.

    Este tipo de migración quirúrgica —buscar patrones muertos, cambiarlos y verificar— es exactamente lo que un agente hace bien si le das el CHANGELOG y los selectores concretos. Es el flujo que enseño en Construye con IA: contexto preciso primero, ejecución después.


    La conclusión que te llevas

    Un release que quita JavaScript casi siempre mueve el estado a otro sitio. Y el CSS que apuntaba al estado viejo no se queja: se apaga. Es el mismo patrón que en los breaking changes de pnpm 12: la nota de release vende el titular y el trabajo real está tres párrafos más abajo.

    Si mantienes un sitio con Starlight, haz hoy los cuatro grep de arriba antes de actualizar. Tardas menos de lo que has tardado en leer este post, y te ahorras la captura de un compañero el lunes por la mañana.

    En Dominicode Labs trabajamos este tipo de migraciones sobre proyectos reales, con el diff delante en vez de con el anuncio de marketing. Y el anuncio original, por si quieres la versión oficial, está en el blog de Astro.


    Preguntas frecuentes

    ¿Cómo actualizo a Starlight 0.42?

    Ejecuta npx @astrojs/upgrade, que sube Starlight, Astro y el resto de integraciones a la vez. Antes de lanzarlo, busca en tu proyecto starlight-menu-button, data-mobile-menu-expanded, aria-expanded en tus estilos y tagline en astro.config: cada coincidencia es una línea que hay que migrar. Necesitas Astro v7.2.10 o superior. Después, abre el sitio en un móvil real y comprueba el botón del menú.

    ¿Starlight 0.42 envía más o menos JavaScript al navegador?

    Menos. El "100 % más de JavaScript" del anuncio se refiere al paquete de npm, que ahora se distribuye compilado a JavaScript en lugar de TypeScript. Lo que llega al navegador de quien lee tu documentación baja, porque el menú móvil se apoya en la Popover API nativa en vez de en código propio. Un sitio Starlight medio sigue enviando entre 3 y 13 veces menos JavaScript al usuario final que MkDocs, Sphinx, VitePress, Nextra, GitBook o Docusaurus.

    ¿Por qué ha dejado de aplicarse mi CSS del menú móvil tras actualizar?

    Porque el botón ya no va envuelto en el custom element <starlight-menu-button> y ya no usa el atributo aria-expanded. Cualquier selector construido sobre esos dos elementos deja de coincidir con nada y el navegador lo ignora en silencio. La migración es apuntar al botón con .sl-menu-button y leer el estado abierto con .sl-menu-button:has(~ :popover-open).

    ¿Por qué el selector nuevo usa :has() en lugar de una clase en el botón?

    Porque el estado ha cambiado de dueño. Antes lo escribía JavaScript en el botón; ahora lo gestiona el navegador en el panel del menú, que es el elemento popover. Para estilar el botón según ese estado necesitas un selector relacional: :has(~ :popover-open) significa "este botón, cuando un hermano posterior suyo está abierto". Para el <body>, el equivalente del antiguo data-mobile-menu-expanded es body:has(sl-sidebar-pane:popover-open).

    ¿Qué versiones mínimas necesito para actualizar a Starlight 0.42?

    Astro v7.2.10 o superior. Si usas @astrojs/markdown-satteri, 0.4.0 o superior. Si usas @astrojs/markdown-remark, 7.3.0 o superior. El comando npx @astrojs/upgrade se encarga de subir Starlight, Astro y el resto de integraciones a la vez.

    ¿Debería preocuparme por los navegadores que pierden soporte?

    Depende de tus analíticas, pero casi nunca. Se cae el soporte oficial de Chromium anterior a la 116 (agosto de 2023), Safari anterior a 17.0 (septiembre de 2023) y Firefox anterior a 125. Son los mínimos que exige la Popover API. En documentación técnica ese tráfico suele ser residual; míralo antes de bloquear la actualización por si acaso.

    ¿Qué hago si tenía JavaScript propio escuchando la apertura del menú?

    Bórralo y escucha el evento nativo. El panel es el popover, así que un panel.addEventListener('toggle', ...) con event.newState === 'open' te da lo mismo que antes conseguías con un MutationObserver sobre aria-expanded, con la mitad de código y sin depender de detalles internos de Starlight.

    ¿Y la opción tagline de la configuración?

    Se ha eliminado y no tiene reemplazo, porque nunca se llegó a usar. Bórrala de tu astro.config al actualizar.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.

  • Cómo automatizar usabilidad con agentes de IA en tu app

    Cómo automatizar usabilidad con agentes de IA en tu app

    Un cliente me enseñó su aplicación de onboarding hace unos meses. Cinco pasos. Diseño limpio. Todo funcional en los tests.

    La tasa de abandono era del 68% en el paso tres.

    Revisamos los tests de integración: pasaban todos. El formulario enviaba datos correctamente. La validación funcionaba. El CI estaba en verde. Y aun así, casi siete de cada diez usuarios salían del flujo antes de terminar.

    El problema no era que la aplicación no funcionara. Era que nadie había auditado cómo se usaba.

    Ahí está la diferencia que muchos developers no distinguen hasta que lo ven en métricas reales: una cosa es que el código haga lo que debe, y otra muy distinta es que el usuario pueda usarlo sin fricción. La primera la resuelves con tests. La segunda la resuelves con automatizar usabilidad con agentes de IA — que es exactamente lo que vamos a ver aquí.

    Automatizar usabilidad con agentes de IA consiste en delegar la auditoría de accesibilidad, flujos de usuario y fricciones de interfaz a un agente — como Claude Code con MCP de Playwright — que controla el navegador de forma programática, navega flujos reales y genera un reporte estructurado de hallazgos sin intervención manual.


    Testing funcional vs. testing de usabilidad

    Un test funcional pregunta: ¿el botón de "Enviar" llama a la función correcta?

    Un test de usabilidad pregunta: ¿el usuario sabe que tiene que hacer clic ahí? ¿Lo ve? ¿Entiende qué va a pasar después?

    Son preguntas distintas y se responden con herramientas distintas.

    Los tests funcionales los escribes tú, los corre un CI, comprueban comportamiento esperado. Eso ya lo sabes hacer. Lo que un agente de IA aporta es la capacidad de navegar tu aplicación como si fuera un usuario — explorar rutas no documentadas, detectar elementos sin etiquetas accesibles, medir cuánto tarda en responder una pantalla, o identificar que un campo de error aparece debajo del scroll y el usuario nunca lo ve.

    No te reemplaza un test de usabilidad con personas reales. Pero te da un nivel de auditoría automatizada que antes no existía — y que tú nunca harías manualmente en cada PR. Si quieres ver cómo encaja esto en un pipeline completo de desarrollo, tienes la explicación en el post sobre automatizar el proceso de desarrollo con IA.


    Qué puede detectar un agente

    Cuando conectas Claude Code con el MCP de Playwright o Chrome DevTools, el agente puede controlar el navegador de forma programática. Eso significa que puede:

    • Navegar a cualquier ruta de tu aplicación
    • Hacer clic en elementos, rellenar formularios, desplazarse por la página
    • Leer el DOM y el árbol de accesibilidad (el que usa un lector de pantalla)
    • Medir tiempos de respuesta entre interacciones
    • Ejecutar axe-core para detectar violaciones WCAG
    • Capturar screenshots en cada paso del flujo
    • Generar un reporte estructurado con los hallazgos

    Lo que un agente detecta bien:

    1. Accesibilidad técnica: imágenes sin alt, botones sin aria-label, contraste insuficiente entre texto y fondo, formularios sin etiquetas asociadas, skip navigation ausente, foco de teclado atrapado en un componente modal.
    2. Fricciones estructurales: mensajes de error fuera del viewport, campos obligatorios que no se identifican como tales hasta el submit, pasos de onboarding que no guardan el progreso si el usuario recarga.
    3. Performance percibida: cuánto tarda en aparecer el primer elemento interactivo después de una navegación, si hay loaders sin indicación de progreso, si el layout shift hace que el usuario haga clic en el elemento equivocado.
    4. Cobertura de flujos: si una ruta de error (credenciales incorrectas, sesión expirada, red caída) termina en una pantalla sin instrucciones claras.

    Cómo configurar el agente para automatizar la auditoría de usabilidad

    La combinación que funciona en producción: Claude Code + MCP de Playwright.

    Para instalarlo: npx @playwright/mcp@latest. Una vez activo en tu configuración de Claude Code, el agente tiene acceso a las herramientas de control del navegador sin que escribas una línea de Playwright.

    El MCP de Playwright expone herramientas al agente para controlar el navegador: browser_navigate, browser_click, browser_type, browser_snapshot, browser_evaluate. Claude puede encadenar esas herramientas para ejecutar flujos completos.

    Un ejemplo de instrucción al agente para auditar el onboarding de una app:

    ## Tarea: Auditoría de usabilidad — flujo de onboarding
    
    URL base: http://localhost:4200
    
    Flujo a auditar:
    1. Navega a /register
    2. Rellena el formulario con datos válidos: nombre, email, contraseña
    3. Haz clic en "Crear cuenta"
    4. Completa los pasos del onboarding hasta llegar al dashboard
    
    En cada paso:
    - Captura un screenshot
    - Extrae el árbol de accesibilidad del contenido principal
    - Identifica elementos interactivos sin aria-label o sin texto visible
    - Mide el tiempo hasta que el siguiente paso es interactivo
    - Detecta si hay mensajes de error o advertencia y si son visibles sin scroll
    
    Al terminar, genera un reporte en formato JSON con esta estructura:
    {
      "paso": string,
      "url": string,
      "tiempo_carga_ms": number,
      "violaciones_accesibilidad": [],
      "fricciones_detectadas": [],
      "screenshot": string
    }
    

    El agente ejecuta eso de forma autónoma. Navega, interactúa, observa, y vuelve con un reporte estructurado.


    Accesibilidad automática con axe-core

    Para violaciones WCAG, la integración más sólida es axe-core. El agente puede ejecutarlo sobre cualquier página activa en el navegador mediante browser_evaluate:

    // Si axe-core no está en el bundle de la app, inyectarlo primero:
    // await page.addScriptTag({ url: 'https://cdn.jsdelivr.net/npm/axe-core/axe.min.js' });
    
    // Ejecutar la auditoría en el contexto de la página
    const results = await axe.run();
    return {
      violaciones: results.violations.map(v => ({
        impacto: v.impact,
        descripcion: v.description,
        elementos: v.nodes.map(n => n.target)
      }))
    };
    

    Lo que devuelve axe-core son violaciones categorizadas por impacto: critical, serious, moderate, minor. El agente puede filtrar solo las críticas, agregar el selector del elemento afectado, y generar una lista accionable para el developer.

    Esto detecta cosas como:

    • Contraste de color insuficiente (ratio menor a 4.5:1 para texto normal)
    • Imágenes sin atributo alt o con alt vacío en imágenes informativas
    • Elementos <div> y <span> usados como botones sin rol ARIA
    • Formularios sin <label> asociado o con placeholder como único identificador
    • Encabezados fuera de jerarquía (<h4> después de <h2> sin <h3>)

    Esta es una de las capacidades que trabajamos en detalle en el curso Construye con IA: cómo delegar auditorías estructuradas al agente para que el developer se centre en las decisiones de producto, no en el checklist técnico.


    El reporte de hallazgos

    Un agente que navega y detecta problemas no sirve de nada si los hallazgos terminan en un log de consola que nadie lee.

    El formato que mejor funciona para integrar en un workflow de desarrollo es un JSON estructurado que puedas convertir en un issue de GitHub, una tarea en Linear, o un comentario en un PR.

    Estructura mínima de reporte que el agente genera:

    {
      "auditoria": {
        "fecha": "2026-06-17",
        "url_base": "https://app.ejemplo.com",
        "flujo": "onboarding",
        "duracion_total_ms": 8420
      },
      "resumen": {
        "violaciones_criticas": 3,
        "violaciones_serias": 7,
        "fricciones_detectadas": 4,
        "pasos_con_retraso": 2
      },
      "hallazgos": [
        {
          "paso": "registro",
          "tipo": "accesibilidad",
          "impacto": "critical",
          "descripcion": "Campo de contraseña sin label asociado. Solo usa placeholder.",
          "selector": "#password-input",
          "referencia_wcag": "1.3.1"
        },
        {
          "paso": "paso-2-perfil",
          "tipo": "friccion",
          "impacto": "serious",
          "descripcion": "Mensaje de validación aparece 280px por debajo del campo en mobile. No visible sin scroll.",
          "selector": ".validation-message",
          "screenshot": "paso-2-error-state.png"
        }
      ]
    }
    

    Este reporte lo puedes consumir directamente en tu pipeline de CI, enviarlo a un webhook de Slack, o procesarlo con otro agente que abra los issues correspondientes.


    Agentes IA vs Lighthouse

    Capacidad Lighthouse Agente IA + MCP Playwright
    Métricas de rendimiento (LCP, CLS, FID) ✅ ❌
    Accesibilidad estática (axe-core) ✅ ✅ más granular
    Flujos interactivos multipaso ❌ ✅
    Estados de error y modales ❌ ✅
    Reporte adaptado al contexto del proyecto ❌ ✅ JSON estructurado
    Requiere código de automatización ❌ ❌ lenguaje natural
    Integración en CI/CD ✅ ✅

    Lighthouse mide el estado de una página en un instante. Un agente mide cómo un usuario real la recorre.


    Lo que el agente no puede hacer

    Esto es importante. Un agente mide lo que puede observar en el DOM y en el comportamiento de la interfaz. No puede medir lo que ocurre dentro del usuario.

    No detecta:

    • Frustración emocional. Si el flujo es técnicamente correcto pero genera ansiedad porque el lenguaje es frío o las instrucciones son ambiguas, el agente no lo sabe.
    • Preferencias estéticas. El contraste puede pasar el ratio WCAG y aun así resultar incómodo visualmente en contextos específicos.
    • Contexto cultural. Un ícono que es intuitivo para un usuario europeo puede no serlo para un usuario latinoamericano. El agente no tiene ese mapa cultural.
    • Carga cognitiva subjetiva. Puede detectar que hay ocho campos en un formulario, pero no puede decirte si eso es demasiado para tu audiencia específica.
    • Microcopy y confianza. El texto de un CTA puede ser técnicamente legible y aun así no generar suficiente confianza para que el usuario haga clic.

    Esas decisiones siguen siendo tuyas — o del diseñador, o del researcher de UX. Lo que el agente elimina es el trabajo de auditoría técnica repetitiva que de otra forma no harías en cada ciclo de desarrollo.


    Cómo integrarlo en tu workflow

    El patrón que funciona sin complicar el pipeline:

    1. Local, bajo demanda: el developer lanza la auditoría sobre la rama antes de abrir el PR. El agente revisa el flujo afectado por el cambio.
    2. En CI, sobre entornos de preview: cada PR despliega a un entorno de preview (Vercel, Netlify, Railway), y el agente audita ese entorno de forma automática antes del merge.
    3. Semanal, sobre producción: un job programado lanza la auditoría completa sobre la app en producción y genera un reporte que llega al equipo.

    El tercer nivel es el más valioso a largo plazo: detecta regresiones de accesibilidad que se cuelan en producción sin que nadie las vea en los tests unitarios.

    El paso de code review automático antes del PR — que complementa esta auditoría de usabilidad — lo explico en detalle en el post sobre agentic code review con Claude Code.

    Si quieres ver cómo construir este tipo de pipelines con agentes desde cero, en Dominicode Labs tenemos proyectos completos que aplican exactamente este enfoque — desde la configuración del MCP hasta la generación del reporte final.


    FAQ

    ¿Necesito conocer Playwright para esto?
    No necesitas escribir código Playwright. El MCP abstrae las herramientas de control del navegador y el agente las usa directamente. Basta con que describas el flujo que quieres auditar en lenguaje natural.

    ¿axe-core cubre todos los criterios WCAG?
    Cubre los criterios que son detectables automáticamente — menos de la mitad de los criterios de WCAG 2.1. El resto requiere evaluación humana. Pero ese 30-40% incluye los problemas más comunes y los más graves.

    ¿El agente puede auditar aplicaciones con autenticación?
    Sí. Puedes darle al agente las credenciales de una cuenta de prueba, o configurar el MCP para que arranque el navegador con una sesión ya autenticada. El agente navega como un usuario real, incluyendo el flujo de login.

    ¿Qué diferencia hay entre esto y Lighthouse?
    Lighthouse audita métricas de performance, SEO básico y accesibilidad en un snapshot estático. Un agente con MCP de Playwright puede auditar flujos interactivos completos — formularios multipaso, modales, estados de error, interacciones con el teclado — y generar reportes adaptados a tu contexto específico, no a un checklist genérico.

    ¿Puedo usar esto con cualquier framework frontend?
    Sí. El agente interactúa con el navegador, no con el framework. Funciona igual con Angular, React, Vue o cualquier app renderizada en el cliente o en el servidor.

    La próxima vez que un cliente te muestre métricas de abandono con todos los tests en verde, ya sabes qué está pasando — y cómo resolverlo.


    Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.