En el Módulo 3 cerramos el recorrido por los cuatro principios POUR entendidos como requisitos: aprendimos qué exige WCAG en cada criterio. Ahora empieza la parte de implementación: cómo se cumple con HTML y CSS reales. Y arrancamos por donde debe arrancarse, porque es la vía más directa, barata y robusta de conseguir accesibilidad: el HTML semántico. Cuando eliges el elemento correcto, el navegador te regala name, role y value (los tres pilares del criterio 4.1.2 que vimos como requisito) sin que escribas ni una línea de JavaScript ni de ARIA. Un <button> ya "es" un botón para el lector de pantalla de Marta; un <nav> ya "es" una región de navegación por la que Lucía puede saltar. La semántica es, literalmente, accesibilidad gratis.

En esta lección construimos el esqueleto de la landing y el catálogo de Cursalia usando los elementos adecuados y veremos, contrastando marcado bueno frente a malo, por qué la etiqueta que eliges cambia por completo la experiencia de las tecnologías de asistencia.

Contenido

  1. Por qué la semántica da name/role/value gratis
  2. Landmarks: las regiones de la página
  3. Jerarquía de encabezados h1–h6
  4. Listas: ul, ol y dl
  5. Tablas de datos accesibles
  6. Botón vs enlace: el error del <div onclick>
  7. Controles de formulario nativos (mención)
  8. Atributos de página: lang, title y el skip link
  9. La landing de Cursalia: buena vs mala estructura
  10. Errores comunes y consejos
  11. Ejercicios
  12. Conclusión

Por qué la semántica da name/role/value gratis

Recordemos del Módulo 2 el árbol de accesibilidad: el navegador construye, a partir de tu HTML, una versión paralela del DOM que expone a las tecnologías de asistencia el rol (qué es el elemento), el nombre (cómo se llama) y el valor/estado (en qué situación está) de cada componente. El HTML semántico es la forma en que "rellenas" ese árbol sin esfuerzo.

Si escribes... El árbol de accesibilidad expone... Coste extra
<button>Matricular</button> role=button, name="Matricular", focusable, activable con Enter/Espacio Cero
<nav> role=navigation (landmark saltable) Cero
<h2>Cursos destacados</h2> role=heading, level=2 (aparece en la lista de encabezados) Cero
<div class="button" onclick> role=generic (¡nada!), no focusable, no activable con teclado Mucho: hay que recrearlo todo con ARIA + JS

La conclusión práctica es la regla de oro de la accesibilidad, que repetiremos en 04-03:

Usa el elemento HTML nativo que ya tiene la semántica que necesitas antes de recrearla con ARIA y JavaScript.

Landmarks: las regiones de la página

Los landmarks (puntos de referencia) son regiones de alto nivel que permiten a un usuario de lector de pantalla como Marta saltar directamente a "el contenido principal" o "la navegación" sin escuchar toda la página. NVDA ofrece una tecla para listar landmarks igual que ofrece una para listar encabezados.

Los elementos de sección de HTML se mapean automáticamente a roles landmark:

Elemento HTML Rol de accesibilidad (landmark) Uso
<header> (a nivel de página) banner Cabecera del sitio: logo, buscador
<nav> navigation Menús de navegación
<main> main Contenido principal (uno por página)
<aside> complementary Contenido relacionado: filtros, promos
<footer> (a nivel de página) contentinfo Pie: avisos legales, contacto
<section> con nombre region Sección temática con encabezado
<article> article Contenido autónomo (una tarjeta de curso)

Ejemplo correcto del armazón de una página de Cursalia:

<body>
  <header>
    <a href="/"><img src="logo-cursalia.svg" alt="Cursalia"></a>
    <nav aria-label="Principal">
      <ul>
        <li><a href="/cursos">Catálogo</a></li>
        <li><a href="/precios">Precios</a></li>
      </ul>
    </nav>
  </header>

  <main>
    <h1>Aprende a tu ritmo con Cursalia</h1>
    <!-- contenido principal -->
  </main>

  <aside aria-label="Filtros del catálogo">
    <!-- filtros -->
  </aside>

  <footer>
    <p>© 2026 Cursalia</p>
  </footer>
</body>

Frente al mismo armazón hecho solo con <div>, que no aporta ninguna región:

<!-- INCORRECTO: para el lector de pantalla esto es una sopa de "grupos genéricos" -->
<div class="header">...</div>
<div class="nav">...</div>
<div class="content">...</div>
<div class="footer">...</div>

Para el lector de Marta, la primera versión tiene cinco regiones nombradas por las que navegar; la segunda no tiene ninguna. Fíjate en el detalle de aria-label="Principal" en el <nav>: si hay más de un landmark del mismo tipo (por ejemplo, la navegación principal y otra en el pie), hay que distinguirlos con un nombre. Esto es semántica reforzada, no reemplazada; lo veremos bien en 04-03.

graph TD
    A[body] --> B["header → banner"]
    A --> C["nav → navigation"]
    A --> D["main → main"]
    A --> E["aside → complementary"]
    A --> F["footer → contentinfo"]
    D --> G["article → article (tarjeta de curso)"]

Jerarquía de encabezados h1–h6

Los encabezados no son "texto grande": son el índice navegable de la página. Marta pulsa la tecla H en NVDA para saltar de encabezado en encabezado y hacerse un mapa mental del contenido, y puede pulsar 2 para ir solo a los de nivel 2. Por eso la jerarquía debe reflejar la estructura lógica, no el tamaño visual (el tamaño es cosa del CSS).

Reglas del criterio 1.3.1 aplicadas a encabezados:

  • Un solo <h1> por página, que describe el tema global.
  • No saltar niveles al bajar: tras un <h2> no se pasa directamente a un <h4>.
  • El nivel indica jerarquía, no apariencia. Si quieres un h2 pequeño, lo achicas con CSS.
<!-- CORRECTO: jerarquía sin saltos -->
<main>
  <h1>Catálogo de cursos</h1>
    <h2>Cursos destacados</h2>
      <h3>Accesibilidad Web</h3>
      <h3>JavaScript moderno</h3>
    <h2>Novedades</h2>
      <h3>Diseño de sistemas</h3>
</main>
<!-- INCORRECTO: se elige el encabezado por su tamaño, no por jerarquía -->
<h1>Catálogo de cursos</h1>
<h4>Cursos destacados</h4>   <!-- salta de 1 a 4 -->
<h2>Accesibilidad Web</h2>   <!-- y ahora sube a 2: el índice queda incoherente -->

Listas: ul, ol y dl

Cuando marcas un conjunto de elementos como lista, el lector de pantalla anuncia "lista de 8 elementos" y permite navegar por ella. Ese contexto se pierde si usas <div> o <br>. Tres tipos:

  • <ul>: lista no ordenada (el orden no importa). El menú de navegación, las tarjetas del catálogo.
  • <ol>: lista ordenada (la secuencia importa). Los pasos de matriculación.
  • <dl>: lista de definiciones (pares término/descripción). La ficha de un curso: "Duración: 20 h", "Nivel: Intermedio".
<!-- CORRECTO: la parrilla de cursos ES una lista -->
<ul class="grid-cursos">
  <li><article>...tarjeta Accesibilidad Web...</article></li>
  <li><article>...tarjeta JavaScript moderno...</article></li>
</ul>

<!-- CORRECTO: metadatos del curso como lista de definiciones -->
<dl>
  <dt>Duración</dt><dd>20 horas</dd>
  <dt>Nivel</dt><dd>Intermedio</dd>
  <dt>Idioma</dt><dd>Español</dd>
</dl>
<!-- INCORRECTO: parece una lista, pero no lo es para nadie más que para el ojo -->
<div class="grid-cursos">
  <div>...tarjeta...</div>
  <div>...tarjeta...</div>
</div>

Tablas de datos accesibles

Una tabla de datos (no de maquetación, que no se debe usar) necesita marcado semántico para que Marta entienda a qué fila y columna pertenece cada celda. Las piezas clave:

  • <caption>: título de la tabla, lo primero que anuncia el lector.
  • <th> para las celdas de encabezado, frente a <td> para datos.
  • scope="col" / scope="row": indica si el encabezado gobierna una columna o una fila.
  • <thead> / <tbody>: separan encabezados del cuerpo.
<!-- CORRECTO: comparativa de planes de Cursalia -->
<table>
  <caption>Comparativa de planes de suscripción</caption>
  <thead>
    <tr>
      <th scope="col">Plan</th>
      <th scope="col">Precio/mes</th>
      <th scope="col">Cursos incluidos</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Básico</th>
      <td>9 €</td>
      <td>50</td>
    </tr>
    <tr>
      <th scope="row">Pro</th>
      <td>19 €</td>
      <td>Todos</td>
    </tr>
  </tbody>
</table>

Sin scope ni <th>, el lector recita una retahíla de números sin decir "Pro, Precio/mes, 19 €": la relación entre dato y encabezado se pierde.

Botón vs enlace: el error del <div onclick>

Es una de las decisiones más equivocadas en la práctica. La regla es sencilla:

  • <a href> (enlace): navega a otro lugar (otra página, otra sección, un ancla). Rol link. El lector lo anuncia como "enlace".
  • <button>: ejecuta una acción en la página actual (abrir el filtro, enviar el formulario, reproducir el vídeo). Rol button. El lector lo anuncia como "botón".

Ambos son focusables y operables con teclado de fábrica: el enlace se activa con Enter, el botón con Enter y Espacio. Un <div> o un <span> con onclick no tiene ninguna de esas propiedades.

<!-- CORRECTO -->
<a href="/cursos/accesibilidad-web">Ver curso de Accesibilidad Web</a>
<button type="button">Abrir filtros</button>
<!-- INCORRECTO: la trampa clásica -->
<div class="btn" onclick="abrirFiltros()">Abrir filtros</div>

Ese <div> falla en cascada para Lucía (que solo usa teclado) y Marta:

  1. No recibe foco: Lucía no puede tabular hasta él (habría que forzar tabindex="0").
  2. No se activa con teclado: aunque tuviera foco, Enter/Espacio no hacen nada (habría que escuchar teclas con JS).
  3. No tiene rol: Marta no oye "botón", oye texto suelto, y no sabe que es pulsable.

Para "arreglar" ese <div> hay que añadir role="button", tabindex="0", manejadores de teclado para Enter y Espacio... es decir, reimplementar a mano todo lo que <button> te da gratis. Recrear controles nativos con ARIA es precisamente lo que veremos en el Módulo 5; aquí la lección es evitar tener que hacerlo.

Controles de formulario nativos (mención)

Los elementos <input>, <select>, <textarea> y <button> traen de serie rol, foco, operación por teclado y estados. Un <input type="checkbox"> ya comunica marcado/desmarcado; un <select> ya es una lista desplegable operable. Aprovecharlos es la base de un formulario accesible, pero su asociación con etiquetas, la agrupación y los errores son el tema completo de la lección 04-02, así que aquí solo dejamos constancia de que existen y de que son la primera opción.

Atributos de página: lang, title y el skip link

Tres detalles del documento con gran impacto:

  • lang en <html>: le dice al lector qué idioma pronunciar. Sin él, NVDA podría leer el español de Cursalia con fonética inglesa y volverse ininteligible. Si dentro hay un fragmento en otro idioma, se marca con lang en ese elemento (esto conecta con la i18n del Módulo 7).
  • <title> de la página: es lo primero que anuncia el lector al cargar y lo que se ve en la pestaña. Debe ser único y descriptivo: Catálogo de cursos · Cursalia.
  • Skip link ("saltar al contenido"): un enlace que es el primer foco de la página y lleva directamente al <main>, para que Lucía no tenga que tabular por toda la cabecera y el menú en cada página.
<!doctype html>
<html lang="es">
<head>
  <title>Catálogo de cursos · Cursalia</title>
</head>
<body>
  <!-- Primer elemento focusable de la página -->
  <a class="skip-link" href="#contenido">Saltar al contenido principal</a>
  <header>...</header>
  <main id="contenido" tabindex="-1">
    <h1>Catálogo de cursos</h1>
  </main>
</body>
</html>

El skip link suele ocultarse visualmente y mostrarse solo al recibir foco, para no molestar al usuario de ratón pero seguir estando disponible para el teclado:

.skip-link {
  position: absolute;
  left: -9999px;      /* fuera de pantalla, pero presente en el DOM */
}
.skip-link:focus {
  left: 1rem;
  top: 1rem;          /* aparece al tabular hacia él */
}

La landing de Cursalia: buena vs mala estructura

Juntemos todo comparando dos versiones de la misma landing.

<!-- INCORRECTO: todo divs, sin encabezados reales, "botones" que son divs -->
<div class="top">
  <div class="menu"><span onclick="ir('/cursos')">Catálogo</span></div>
</div>
<div class="hero">
  <div class="big">Aprende a tu ritmo</div>
  <div class="btn" onclick="registro()">Empieza gratis</div>
</div>
<div class="cards">
  <div class="card">Accesibilidad Web</div>
  <div class="card">JavaScript moderno</div>
</div>

Para Marta esto es una página sin regiones, sin encabezados y sin controles: imposible de recorrer con eficiencia. Para Lucía, nada recibe foco.

<!-- CORRECTO: mismo diseño visual, semántica completa -->
<a class="skip-link" href="#contenido">Saltar al contenido</a>
<header>
  <nav aria-label="Principal">
    <ul><li><a href="/cursos">Catálogo</a></li></ul>
  </nav>
</header>
<main id="contenido">
  <h1>Aprende a tu ritmo con Cursalia</h1>
  <p><a class="cta" href="/registro">Empieza gratis</a></p>

  <section aria-labelledby="destacados">
    <h2 id="destacados">Cursos destacados</h2>
    <ul class="cards">
      <li>
        <article>
          <h3><a href="/cursos/accesibilidad-web">Accesibilidad Web</a></h3>
          <img src="acc.webp" alt="">   <!-- alt en 05-04 -->
        </article>
      </li>
      <li>
        <article>
          <h3><a href="/cursos/javascript">JavaScript moderno</a></h3>
        </article>
      </li>
    </ul>
  </section>
</main>

El CSS puede hacer que ambas se vean idénticas. La diferencia no está en los píxeles, sino en la información estructural que una expone y la otra destruye. (El alt="" de la imagen tiene sentido y lo estudiaremos a fondo en 05-04.)

Errores Comunes y Consejos

  • Usar <div>/<span> para todo. Es el error raíz. Antes de escribir un <div>, pregúntate si existe un elemento que signifique lo que quieres decir.
  • Elegir el encabezado por su tamaño. Un <h3> no es "un título mediano"; es un nivel jerárquico. Ajusta el tamaño con CSS, nunca cambiando el nivel.
  • Varios <h1> o saltos de nivel. Rompen el índice de la página. Un <h1>, y baja de uno en uno.
  • Botón que navega o enlace que actúa. Si cambia de URL, es <a>; si actúa en la página, es <button>. No los intercambies por estética.
  • Tablas para maquetar. Reserva <table> para datos tabulares; usa CSS Grid/Flexbox para la disposición visual.
  • Olvidar lang y <title>. Son una línea cada uno y mejoran drásticamente la lectura por voz.
  • Consejo: navega tu propia página solo con el teclado (Tab, Enter) y con la lista de encabezados de un lector. Si no puedes recorrerla, tu semántica está incompleta.

Ejercicios

Ejercicio 1: convertir un div-soup en landmarks

Tienes este fragmento de la cabecera de Cursalia. Reescríbelo con los elementos semánticos correctos, añadiendo el nombre necesario al nav.

<div class="header">
  <div class="logo">Cursalia</div>
  <div class="menu">
    <div class="item"><span onclick="ir('/cursos')">Catálogo</span></div>
    <div class="item"><span onclick="ir('/precios')">Precios</span></div>
  </div>
</div>

Ejercicio 2: arreglar la jerarquía de encabezados

Un maquetador eligió los encabezados por su tamaño visual. Corrige los niveles para que la jerarquía sea lógica y sin saltos.

<h1>Ficha del curso: Accesibilidad Web</h1>
<h3>Descripción</h3>
<h2>Temario</h2>
<h4>Módulo 4: HTML y CSS</h4>

Ejercicio 3: botón o enlace

Para cada control, decide si debe ser <a href> o <button> y escríbelo:

  1. "Ir a la página de precios".
  2. "Reproducir la vídeo-lección" (en la misma página).
  3. "Descargar el certificado" (navega a un PDF).
  4. "Abrir el desplegable de filtros del catálogo".

Soluciones

Solución 1. Cada <div> estructural se sustituye por su equivalente semántico; los "items" pulsables que navegan son enlaces reales:

<header>
  <a href="/" class="logo">Cursalia</a>
  <nav aria-label="Principal">
    <ul>
      <li><a href="/cursos">Catálogo</a></li>
      <li><a href="/precios">Precios</a></li>
    </ul>
  </nav>
</header>

El aria-label="Principal" es necesario porque probablemente haya otro <nav> (por ejemplo en el pie) y hay que distinguirlos.

Solución 2. Debe bajar de forma progresiva sin saltos y con un solo <h1>:

<h1>Ficha del curso: Accesibilidad Web</h1>
<h2>Descripción</h2>
<h2>Temario</h2>
<h3>Módulo 4: HTML y CSS</h3>

"Descripción" y "Temario" son secciones hermanas de primer nivel (h2); el módulo cuelga del temario (h3).

Solución 3.

  1. Enlace: navega a otra página → <a href="/precios">Ir a precios</a>.
  2. Botón: acciona algo en la misma página → <button type="button">Reproducir</button>.
  3. Enlace: lleva a un recurso (el PDF) → <a href="/certificado.pdf" download>Descargar certificado</a>.
  4. Botón: abre/cierra un panel en la página → <button type="button" aria-expanded="false">Filtros</button>. El aria-expanded y su comportamiento los veremos en 04-03 y 05-01.

Conclusión

El HTML semántico es el cimiento de todo lo demás en accesibilidad: eligiendo bien los elementos obtienes name, role y value gratis, regiones navegables, un índice de encabezados y controles operables con teclado, sin escribir código adicional. Hemos maquetado la landing de Cursalia con landmarks, jerarquía de encabezados, listas, tablas de datos, enlaces y botones correctos, y hemos fijado la regla de oro: usa el elemento nativo antes que recrearlo con ARIA (idea que ampliaremos en 04-03).

El siguiente paso natural es el punto donde la semántica se vuelve más delicada y donde se concentran los criterios 3.3.x: los formularios. En la lección 04-02 · Formularios Accesibles aplicaremos todo esto al formulario de matrícula de Cursalia —etiquetas, agrupaciones, campos obligatorios, ayudas y mensajes de error— para que Marta, Hugo y Lucía puedan completarlo sin barreras.

© Copyright 2026. Todos los derechos reservados