Next.js
Accesibilidad en aplicaciones Next.js
Explica accesibilidad en navegación, streaming, formularios, modales, metadata y estados dinámicos de Next.js mediante semántica, foco y anuncios.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Next.js
Explica accesibilidad en navegación, streaming, formularios, modales, metadata y estados dinámicos de Next.js mediante semántica, foco y anuncios.
Next.js puede producir HTML rápido, navegar sin recargar y transmitir contenido progresivamente, pero ninguna de esas capacidades garantiza accesibilidad. Debes conservar semántica, foco, nombres, estados y continuidad cuando cambia la ruta, aparece un fallback, falla una Action o se abre una capa interceptada.
La accesibilidad se diseña en varias capas:
HTML semantics
+ React component behavior
+ Next.js navigation/rendering
+ CSS visual states
+ content and testingUn componente accesible aislado puede volverse inaccesible si la navegación no mueve el foco, el loading borra contexto o un modal interceptado deja interactuar con el fondo.
Root layout:
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es">
<body>
<a className="skip-link" href="#main-content">
Saltar al contenido principal
</a>
<SiteHeader />
{children}
</body>
</html>
);
}Page:
export default function ServicesPage() {
return (
<main id="main-content" tabIndex={-1}>
<h1>Servicios de desarrollo web</h1>
{/* ... */}
</main>
);
}Cada documento debe tener landmarks y una jerarquía de headings comprensible. Layouts anidados no deben crear múltiples <main> para una misma vista salvo una razón semántica válida.
<Link href="/portfolio">Ver proyectos</Link>
<button type="button" onClick={openFilters}>Abrir filtros</button>No uses div onClick, ni button con router.push cuando un link real aporta apertura en nueva pestaña, URL y semántica.
Link actualiza el árbol sin una recarga completa. El usuario necesita comprender que cambió la página.
Next.js gestiona scroll y focus bajo sus reglas, pero una app compleja debe probar:
No muevas foco en cada cambio menor de search params si la experiencia permanece en la misma pantalla.
Una estrategia puede actualizar el título del documento mediante Metadata API y mostrar un heading visible. Los lectores de pantalla suelen anunciar cambios de title/navegación de forma variable.
Para SPA compleja, una live region de route announcements puede ayudar, pero evita duplicar anuncios con focus y title. Prueba lectores reales antes de agregar mensajes globales.
loading.tsx y Suspense fallbacks deben mantener contexto:
export default function Loading() {
return (
<main aria-busy="true" aria-labelledby="page-title">
<h1 id="page-title">Pedidos</h1>
<OrdersSkeleton aria-hidden="true" />
<p className="sr-only" role="status">
Cargando pedidos…
</p>
</main>
);
}No marques cada skeleton block como status. Un único anuncio es suficiente.
Mantén dimensiones para evitar CLS y no envíes foco al fallback.
Durante una transición de filtros, conservar resultados anteriores con aria-busy puede ser menos disruptivo que reemplazarlos por un skeleton:
<section aria-busy={isPending} aria-labelledby="results-title">
<h2 id="results-title">Resultados</h2>
<Results />
</section>Comunica que se actualizan sin borrar contenido útil.
<section role="alert" aria-labelledby="error-title">
<h2 id="error-title">No pudimos cargar los pedidos</h2>
<p>Tu información no se perdió.</p>
<button onClick={reset}>Reintentar</button>
</section>No uses role="alert" para mensajes no urgentes o cada validación mientras se escribe.
Después de un error de form, mueve foco al resumen o primer campo inválido cuando sea útil.
<label htmlFor="email">Correo electrónico</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
aria-invalid={Boolean(error)}
aria-describedby={error ? "email-error" : undefined}
/>
{error && <p id="email-error">{error}</p>}useActionState no cambia requisitos HTML:
No deshabilites todos los campos si el usuario necesita copiar o revisar lo enviado. aria-busy y un botón pending pueden bastar.
Una route modal necesita:
role="dialog" o elemento <dialog> correctamente usado.aria-modal="true".aria-labelledby.Intercepting Routes solo resuelven contexto/URL; no implementan el patrón dialog.
Un menú de navegación simple suele ser una lista de links, no role="menu". ARIA menu implica un modelo de teclado de aplicación (flechas, roving tabindex) que no corresponde a la navegación web común.
Usa:
<nav aria-label="Navegación principal">
<ul>
<li><NavLink href="/">Inicio</NavLink></li>
</ul>
</nav>Marca página actual con aria-current="page".
<nav aria-label="Migas de pan">
<ol>
<li><Link href="/portfolio">Proyectos</Link></li>
<li aria-current="page">DomiSys</li>
</ol>
</nav>Los breadcrumbs ayudan a comprender jerarquía. Structured data puede acompañarlos, pero no reemplaza la navegación visible.
next/image requiere alt:
alt="".No repitas el mismo título dos veces en el nombre accesible de una card. Evalúa el árbol completo.
<button type="button" aria-label="Eliminar proyecto">
<TrashIcon aria-hidden="true" />
</button>Si existe texto visible, normalmente no necesitas aria-label. Un aria-label diferente puede ocultar el texto visible para tecnologías de asistencia y afectar voice control.
<html lang> correcto.dir para RTL.Metadata ayuda a contexto, pero los headings visibles siguen siendo necesarios.
display:none si debe anunciarse.CSS Grid order puede mostrar una secuencia distinta a teclado/lector.
Producen HTML semántico sin JavaScript cliente. Esto puede mejorar robustez y tiempo hasta contenido, pero un control que necesita evento debe ser Client Component.
No conviertas un link en Client Component solo para animarlo; CSS puede hacerlo.
HTML puede aparecer antes de hydration. Usa elementos nativos para que links/forms tengan comportamiento útil lo antes posible.
Un botón con handler cliente visible antes de hydration puede no responder. Reduce bundle y evita aparentar disponibilidad crítica si el JS tarda.
Un form con Server Action puede enviar sin JavaScript bajo ciertos flujos. Esto mejora resiliencia y accesibilidad, pero no significa que toda experiencia deba funcionar sin JS.
Prioriza semántica nativa y añade mejoras:
HTML base
→ server validation
→ client pending
→ optimistic UI optionalDatos tabulares usan table real:
<table>
<caption>Pedidos recientes</caption>
<thead>
<tr>
<th scope="col">Número</th>
<th scope="col">Estado</th>
</tr>
</thead>
<tbody>{/* rows */}</tbody>
</table>Responsive no debe convertirla en divs sin relaciones. Puedes ofrecer scroll, vista alternativa o labels.
Colecciones usan ul/ol cuando el orden/agrupación importa.
Problemas:
Ofrece paginación o botón “Cargar más”, actualiza URL cuando corresponde y anuncia cuántos items se agregaron.
role="status" para confirmación.role="alert" para error urgente.Un toast de éxito no necesita robar foco.
Login debe usar autocomplete (username, current-password, new-password, one-time-code) y permitir password managers/paste.
CAPTCHA debe tener alternativa accesible. Los errores no deben revelar cuenta existente.
Automatización no detecta foco lógico, calidad de alt o comprensión.
<nav> y links con aria-current.No teclado ni semántica.
Sobrescribe texto visible.
Borra estructura y contexto.
No comunica destino.
Teclado escapa.
Bloquea zoom.
Impide navegación y footer.
La API del componente ya quedó mal diseñada.
role="menu" suele ser incorrecto para navbar?Progressive enhancement y UX de navegación conecta estas garantías con prefetch, pending, redirects, offline y feedback de interacción.