study.plynte.com
@franguh
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)

09 · HTML, CSS y accesibilidad#

Objetivo#

Al terminar esta lección podrás construir interfaces web a partir de su estructura y controles nativos antes de decidir colores o espaciado. Comprenderás por qué un formulario semántico ofrece interoperabilidad gratuita con teclados y tecnologías de asistencia, cómo el Árbol de Accesibilidad (AOM) traduce el DOM para lectores de pantalla, y cómo diagnosticar y solucionar problemas de diseño (layout) mediante el modelo de caja CSS (box model) sin recurrir a márgenes o parches arbitrarios.


Antes de empezar#

  1. No necesitas React, Node.js ni TypeScript para esta lección. Basta con guardar un archivo .html y abrirlo directamente en cualquier navegador moderno (Chrome, Firefox, Safari o Edge).
  2. Las herramientas de desarrollo del navegador (DevTools, tecla F12 o clic derecho -> Inspeccionar) te permitirán auditar el Árbol de Accesibilidad, los estilos computados y el foco de teclado en tiempo real.
  3. El primer bloque de código de esta lección es un documento HTML completo y ejecutable; el bloque posterior es un fragmento de estilos CSS diseñado para complementar la estructura.
  4. Si encuentras algún concepto técnico desconocido, consulta el glosario técnico.

Diccionario de conceptos de la lección (Glosario desde cero)#

1. HTML semántico y Landmarks (<main>, <form>, <label>, <button>) vs Contenedores genéricos (<div>, <span>)#

  • Intuición cotidiana: En un periódico impreso, los titulares tienen tipografía grande, las columnas dividen las noticias y el pie de imprenta indica los créditos. Si todo el periódico estuviera impreso en párrafos grises idénticos sin encabezados ni secciones, un lector no sabría dónde empieza una noticia o dónde termina una opinión.
  • Definición técnica precisa: HTML semántico es el uso de elementos estándar cuyos nombres comunican el significado y propósito de su contenido tanto al navegador como a motores de búsqueda y tecnologías de asistencia. Los elementos de hito o landmarks (como <main>, <nav>, <header>, <form>) dividen el documento en regiones navegables lógicas. Los contenedores genéricos (<div> a nivel de bloque y <span> a nivel de línea) carecen de valor semántico (semantic zero): son puras cajas neutras de agrupamiento para aplicar CSS o interactuar con scripts.
  • Ejemplo en código:
HTML
  <!-- Semántico: comunica región principal y formulario interactivo -->
  <main>
    <form action="/tasks" method="post">
      <label for="task-title">Task title</label>
      <input id="task-title" name="title" type="text" required>
      <button type="submit">Create task</button>
    </form>
  </main>

  <!-- Genérico (Antipatrón): carece de significado estructural para el navegador -->
  <div class="main-content">
    <div class="form-wrapper">
      <span class="label">Task title</span>
      <input class="input">
      <div class="btn" onclick="submitForm()">Create task</div>
    </div>
  </div>
  • Concepción errónea: Pensar que <div> y <section> son intercambiables. Un <div> no transmite ninguna estructura semántica ni aparece como punto de salto en tecnologías de asistencia.

2. Árbol DOM vs Árbol de Accesibilidad (AOM / Accessibility Tree)#

  • Intuición cotidiana: El plano arquitectónico de un edificio (DOM) incluye cada tubería, cable y viga estructural. La señalética táctil y auditiva para personas con discapacidad visual (Árbol de Accesibilidad) omite las vigas ocultas y se enfoca exclusivamente en dónde están las puertas, los timbres, las escaleras y los carteles informativos.
  • Definición técnica precisa:
  • *Árbol DOM (Document Object Model):* Estructura de datos en memoria en forma de árbol que contiene la totalidad de nodos del documento (elementos, nodos de texto, comentarios) procesados por el motor del navegador.
  • *Árbol de Accesibilidad (Accessibility Tree o AOM): Estructura derivada creada por el motor del navegador en paralelo al DOM. Filtra elementos puramente visuales y expone a las APIs de accesibilidad del sistema operativo (UI Automation en Windows, AXPlatformNode en macOS/iOS, AT-SPI en Linux) cuatro propiedades críticas de cada objeto interactivo: 1. Rol (Role): Qué es el elemento (ej. button, checkbox, landmark, heading). 2. Nombre accesible (Accessible Name): Cómo se identifica (ej. "Save note"). 3. Estado (State): Condición actual (ej. focused, disabled, expanded, invalid). 4. Valor (Value):* Magnitud o texto contenido (ej. el texto dentro de un <input>).
  • Ejemplo estructural:
TEXT
  DOM:              <button type="submit">Save note</button>
                      └── TextNode: "Save note"

  Accessibility:    Role: button
                    Name: "Save note"
                    Focusable: true
                    Actions: [Press, Focus]
  • Concepción errónea: Creer que un lector de pantalla lee visualmente el CSS dibujado en la pantalla. Los lectores de pantalla consultan exclusivamente el Árbol de Accesibilidad provisto por el sistema operativo.

3. Nombre accesible (Accessible Name) y asociación explícita for/id#

  • Intuición cotidiana: Si hay dos buzones de correo idénticos en la pared y uno tiene un rótulo con tu nombre atornillado firmemente sobre la ranura, sabes exactamente dónde depositar tu carta. Si el rótulo está pegado en una hoja volátil en la pared contigua, nadie puede asegurar a qué buzón corresponde.
  • Definición técnica precisa: El nombre accesible es la etiqueta textual con la que un control interactivo es identificado en el Árbol de Accesibilidad, calculada mediante el algoritmo oficial Accessible Name and Description Computation del W3C. La asociación explícita conecta el atributo for de una etiqueta <label> con el identificador único id de un elemento interactivo (<input>, <select>, <textarea>). Esta conexión programática produce dos efectos fundamentales:
  1. Asigna el texto de la etiqueta como el nombre accesible oficial del campo en el AOM.
  2. Aumenta el área táctil y de puntero: al hacer clic o pulsar sobre el texto de la etiqueta, el navegador transfiere automáticamente el foco de entrada al control asociado.
  • Ejemplo en código:
HTML
  <!-- Asociación explícita y robusta -->
  <label for="task-deadline">Due date</label>
  <input id="task-deadline" name="due_date" type="date">
  • Concepción errónea: Asumir que el atributo placeholder="Enter title" puede sustituir al elemento <label>. El placeholder desaparece en cuanto el usuario teclea el primer caracter (provocando pérdida de contexto), tiene bajo contraste visual por defecto y muchas tecnologías de asistencia no lo calculan de forma consistente como nombre accesible.

4. Orden de tabulación (Tab Order), Foco de teclado y :focus-visible#

  • Intuición cotidiana: Es el recorrido secuencial al avanzar en una fila bancaria: pasas ordenadamente de la puerta al dispensador de turnos, luego a la ventanilla y finalmente a la salida. Si alguien te empuja directamente al sótano o las ventanillas están apagadas y no sabes quién te atiende, la experiencia es caótica.
  • Definición técnica precisa:
  • *Orden de tabulación (Tab Order):* Secuencia predeterminada en la que los elementos interactivos reciben el foco al presionar la tecla Tab (hacia adelante) o Shift + Tab (hacia atrás). En el flujo estándar, sigue exactamente el orden de aparición de los elementos en el árbol DOM.
  • *Foco (Focus):* Estado del navegador que determina qué elemento está activo y preparado para recibir eventos de entrada del teclado.
  • :focus-visible: Pseudo-clase de CSS que aplica estilos de foco únicamente cuando el navegador determina heurísticamente que el usuario se beneficia de un indicador visual (principalmente cuando la navegación se realiza mediante teclado o tecnologías de apoyo), evitando que aparezca un anillo de foco no deseado al hacer clic con el ratón sobre un botón.
  • Ejemplo en código:
CSS
  /* Antipatrón destructivo */
  button:focus {
    outline: none; /* Elimina la orientación para usuarios de teclado */
  }

  /* Práctica profesional accesible */
  button:focus-visible,
  input:focus-visible {
    outline: 3px solid #005fcc;
    outline-offset: 2px;
  }
  • Concepción errónea: Usar outline: none para "limpiar el diseño" sin ofrecer un reemplazo visual. Eliminar el indicador de foco hace la aplicación completamente inusable para cualquier persona que opere sin ratón (usuarios de teclado, conmutadores o lectores de pantalla).

5. Atributos WAI-ARIA (aria-describedby, aria-invalid, role="alert") y la Primera Regla de ARIA#

  • Intuición cotidiana: ARIA es como colocar subtítulos y etiquetas en braille adicionales en un aparato cuando el diseño físico no es capaz de comunicar su función por sí mismo. Sin embargo, si puedes construir el aparato con un botón físico estándar que cualquiera reconoce al tacto, añadirle una etiqueta redundante en braille no aporta nada y puede confundir.
  • Definición técnica precisa: WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) es una especificación técnica del W3C que proporciona atributos semánticos suplementarios (role, aria-*) para enriquecer el Árbol de Accesibilidad cuando los elementos nativos de HTML no son suficientes.
  • Primera Regla de ARIA: "Si puedes usar un elemento o atributo nativo de HTML que ya posea la semántica y comportamiento que requieres, hazlo en lugar de reinventarlo y añadir ARIA".
  • aria-describedby: Enlaza el id de un texto complementario o de ayuda al control. El lector de pantalla anunciará primero el nombre accesible y luego la descripción asociada.
  • aria-invalid="true": Comunica formalmente al Árbol de Accesibilidad que el valor del campo no cumple con los criterios de validación.
  • role="alert": Declara una región viva (live region) asertiva. Cualquier texto insertado dentro de este contenedor es anunciado inmediatamente por el lector de pantalla sin esperar a que el usuario mueva el foco al mensaje.
  • Ejemplo en código:
HTML
  <label for="email">Work email</label>
  <input 
    id="email" 
    name="email" 
    type="email" 
    aria-invalid="true" 
    aria-describedby="email-error"
  >
  <p id="email-error" role="alert">Please enter a valid company email address.</p>
  • Concepción errónea: Pensar que añadir role="button" a un <div> lo convierte en un botón interactivo. ARIA solo altera la información comunicada al Árbol de Accesibilidad; no añade eventos de teclado, no gestiona foco ni ejecuta lógica alguna.

6. Modelo de caja CSS (Box Model: Content, Padding, Border, Margin)#

  • Intuición cotidiana: Imagina un cuadro colgado en la pared:
  • Content: El lienzo con la pintura en sí.
  • Padding: El paspartú o marco blanco de cartón que separa la pintura del marco exterior.
  • Border: El marco físico de madera que rodea y protege la obra.
  • Margin: La distancia de pared vacía obligatoria entre este cuadro y el cuadro de al lado.
  • Definición técnica precisa: Modelo geométrico fundamental del motor de renderizado de CSS que representa cada elemento del documento como una caja rectangular compuesta por cuatro capas concéntricas:
  1. *Área de contenido (Content Area):* Espacio donde se dibujan el texto, imágenes o elementos hijos. Sus dimensiones son width y height.
  2. *Relleno (Padding):* Espacio interno transparente entre el contenido y el borde. Adopta el color de fondo (background-color) del elemento.
  3. *Borde (Border):* Línea perimetral que delimita el padding y el contenido. Posee grosor, estilo y color.
  4. *Margen (Margin): Espacio exterior transparente que separa la caja de los elementos adyacentes en el flujo. No adopta el fondo del elemento y puede colapsar verticalmente con márgenes vecinos (margin collapsing*).
  • Ejemplo esquemático:
TEXT
  +-------------------------------------------------+  ^
  | MARGIN                                          |  |
  |   +-----------------------------------------+   |  |
  |   | BORDER                                  |   |  |
  |   |   +---------------------------------+   |   |  |
  |   |   | PADDING                         |   |   |  Total Box
  |   |   |   +-------------------------+   |   |   |  Dimensions
  |   |   |   | CONTENT (width, height) |   |   |   |  |
  |   |   |   +-------------------------+   |   |   |  |
  |   |   +---------------------------------+   |   |  |
  |   +-----------------------------------------+   |  |
  +-------------------------------------------------+  v
  • Concepción errónea: Confundir margin y padding. El padding expande el área interactiva y el fondo del elemento; el margin empuja a otros elementos lejos de la caja sin incrementar su superficie táctil o visual.

7. box-sizing: content-box vs border-box#

  • Intuición cotidiana:
  • content-box: Pides una mesa de 1 metro de ancho. El carpintero fabrica un tablero de 1 metro, pero luego le clava patas exteriores de 10 cm y un protector de 5 cm. La mesa terminada mide 1.30 metros y ya no entra por la puerta de tu comedor.
  • border-box: Pides una mesa de 1 metro de ancho. El carpintero ajusta el tablero y las patas para que el mueble terminado mida exactamente 1 metro de borde a borde exterior. Entra perfectamente en tu comedor.
  • Definición técnica precisa: Propiedad CSS que determina cómo el navegador calcula el ancho (width) y alto (height) finales renderizados de un elemento:
  • content-box (valor predeterminado histórico de la W3C): La propiedad width aplica únicamente al área de contenido. El padding y el borde se suman hacia afuera:
    Ancho total renderizado = width + padding-left + padding-right + border-left + border-right
  • border-box (modelo moderno): La propiedad width determina el ancho total exterior de la caja incluyendo padding y borde. El área de contenido se encoge internamente para acomodar los rellenos:
    Ancho total renderizado = width
    Ancho del área de contenido = width - (padding-left + padding-right + border-left + border-right)
  • Ejemplo en código:
CSS
  /* Reset universal estándar en la industria */
  *, *::before, *::after {
    box-sizing: border-box;
  }
  • Concepción errónea: Pensar que border-box incluye el margen (margin). Ni content-box ni border-box incluyen el margen exterior en el cálculo de width; el margen siempre se proyecta fuera de la caja.

8. Flujo normal de documento vs Flexbox y CSS Grid#

  • Intuición cotidiana:
  • Flujo normal: Escribir a mano en un cuaderno de rayas: los renglones se apilan naturalmente de arriba abajo y las palabras fluyen de izquierda a derecha sin necesidad de herramientas especiales.
  • Flexbox: Una hilera de cuentas elásticas en un collar: ideal para organizar elementos en una sola dirección (fila o columna), distribuyendo el espacio sobrante o alineándolos verticalmente al centro.
  • CSS Grid: Una hoja de cálculo de Excel: una cuadrícula bidimensional de filas y columnas rígidas o elásticas donde puedes ubicar componentes exactamente en coordenadas (x, y).
  • Definición técnica precisa:
  • *Flujo normal (Normal Flow):* Mecanismo base de disposición en el que los elementos de bloque ocupan el 100% del ancho de su contenedor y se apilan verticalmente, mientras que los elementos de línea se colocan horizontalmente uno tras otro respetando el orden del DOM.
  • *Flexbox (Flexible Box Layout): Modelo de diseño unidimensional optimizado para la alineación y distribución de espacio entre elementos a lo largo de un eje principal (main axis) o eje transversal (cross axis*).
  • *CSS Grid (Grid Layout):* Modelo de diseño bidimensional diseñado para estructurar sistemas de interfaz complejos simultáneamente a lo largo de filas y columnas.
  • Ejemplo en código:
CSS
  /* Flexbox: ideal para barra de acciones */
  .button-group {
    display: flex;
    gap: 0.5rem;
    justify-content: flex-end;
  }

  /* CSS Grid: ideal para formulario en cuadrícula estructurada */
  .task-form {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
  }
  • Concepción errónea: Forzar display: flex para apilar elementos verticalmente en un formulario cuando los elementos de bloque en el flujo normal ya hacen exactamente eso de manera nativa sin código CSS adicional.

Modelo mental y desempacado técnico paso a paso#

1. El abismo entre <button> nativo y <div class="btn">: Los 7 mecanismos gratuitos#

Cuando un desarrollador escribe <div class="btn" onclick="save()">Save</div> para "tener más control visual", está reemplazando un elemento nativo del navegador por una caja muda. Para que ese <div> alcance la paridad funcional real con un <button type="submit">, se tendrían que reconstruir manualmente 7 mecanismos complejos, los cuales casi siempre se implementan de forma incompleta o defectuosa:

DIAGRAMA MERMAID
flowchart TD
    subgraph NativeButton["<button type='submit'> Nativo"]
        direction TB
        N1["1. Foco en Tab Order automático"]
        N2["2. Activación con Enter y Espacio"]
        N3["3. Rol y estados en Accessibility Tree"]
        N4["4. Disparo de Submit en formulario"]
        N5["5. Atributo disabled cancela eventos"]
        N6["6. Clic sintético por tecnologías de apoyo"]
        N7["7. Soporte de Alto Contraste del SO"]
    end

    subgraph DivButton["<div class='btn'> Casero"]
        direction TB
        D1["tabindex='0'"] -.-> D2["keydown listener: Enter + Space"]
        D2 -.-> D3["preventDefault() para scroll de Espacio"]
        D3 -.-> D4["role='button' manual"]
        D4 -.-> D5["form.requestSubmit() manual"]
        D5 -.-> D6["aria-disabled + pointer-events: none"]
        D6 -.-> D7["Forced-colors CSS hack"]
    end

Examinemos en profundidad cada uno de estos 7 mecanismos:

  1. Foco en el orden de tabulación nativo: - Nativo: El navegador incluye <button> automáticamente en la secuencia de Tab. No requiere atributos adicionales. - Con <div>: Debes añadir manualmente tabindex="0". Si olvidas este atributo, el usuario de teclado jamás podrá alcanzar el control.
  2. Activación de teclado dual (Enter y Barra Espaciadora): - Nativo: Un <button> se activa tanto con la tecla Enter como con Space. Además, cuando se presiona Space, el navegador previene nativamente el comportamiento predeterminado de la página (que es hacer scroll vertical) y dispara la acción al liberar la tecla (keyup). - Con <div>: Debes registrar un evento keydown, inspeccionar event.key === "Enter" || event.key === " ", llamar manualmente a event.preventDefault() únicamente cuando sea la barra espaciadora para evitar que la página salte, y manejar el ciclo entre pulsación y liberación.
  3. Exposición automática de rol y propiedades en el AOM: - Nativo: Expone de inmediato Role: button, su nombre accesible y sus estados a las APIs de accesibilidad del sistema operativo. - Con <div>: Requiere role="button". Sin esto, un lector de pantalla leerá el texto como un fragmento estático de contenido, sin informar al usuario de que se trata de un elemento accionable.
  4. *Envío implícito del formulario ancestro (Form Submission): - Nativo: Al estar dentro de un <form>, presionar el botón o pulsar Enter dentro de cualquier campo de texto del formulario dispara el evento submit nativo del formulario, ejecutando la validación del navegador. - Con <div>:* No tiene conexión con el formulario. Tienes que buscar el formulario con JavaScript (document.querySelector o closest("form")) e invocar manualmente form.requestSubmit().
  5. *Manejo del estado deshabilitado (Disabled State): - Nativo: Al colocar el atributo disabled, el navegador elimina automáticamente el botón de la secuencia de Tab, ignora cualquier evento de clic de ratón o pulsación de teclado, y anuncia el estado "deshabilitado" al lector de pantalla. - Con <div>:* Añadir una clase CSS .disabled solo cambia el color. El <div> sigue recibiendo clics, sigue recibiendo foco y el lector de pantalla jamás sabrá que está inactivo, a menos que sincronices aria-disabled="true", manipules tabindex="-1", agregues pointer-events: none en CSS y verifiques la bandera en el manejador de JavaScript.
  6. Eventos de clic sintetizados por accesibilidad: - Nativo: Dispositivos de pulsación para personas con tetraplejia (switch access), comandos de control por voz (como Apple Voice Control) o lectores de pantalla disparan eventos de clic sintéticos sobre los botones reconocidos en el AOM. - Con <div>: Si el rol o los escuchadores de eventos no coinciden exactamente con la especificación, el dispositivo de asistencia no puede accionar el control.
  7. *Compatibilidad con el modo de contraste forzado del sistema operativo (Forced Colors Mode): - Nativo: En Windows High Contrast Mode, el navegador sustituye los colores de fondo y bordes de los botones nativos por los colores del tema del sistema (por ejemplo, ButtonText y ButtonFace), garantizando legibilidad total. - Con <div>:* El navegador lo trata como un bloque decorativo ordinario, suprimiendo sus fondos o haciéndolo completamente invisible si dependía de sombras o colores específicos.

2. Matemática del modelo de caja: ¿Por qué desborda content-box?#

El problema de diseño más común en interfaces principiantes ocurre al asignar width: 100% a un campo de entrada o contenedor y luego aplicarle padding o borde. Analicemos la fórmula matemática exacta:

Imaginemos un contenedor padre cuyo ancho útil disponible es de exactamente 500 píxeles (W_{padre} = 500 px). Le aplicamos a un hijo las siguientes reglas:

CSS
.input-field {
  width: 100%;
  padding: 16px;
  border: 2px solid #333;
}

Escenario A: Bajo box-sizing: content-box (Predeterminado de CSS)#

Bajo content-box, width: 100% le ordena al motor de renderizado: "Haz que el área de contenido interior mida el 100% del ancho del padre (500 px)". Luego, el padding y los bordes se suman hacia afuera:

begin{aligned} W_{total} &= width + padding-left + padding-right + border-left + border-right W_{total} &= 500 px + 16 px + 16 px + 2 px + 2 px W_{total} &= 500 px + 32 px + 4 px = 536 px end{aligned}

Consecuencia geométrica:

Desbordamiento (Overflow) = W_{total} - W_{padre} = 536 px - 500 px = +36 px

La caja del campo sobresale 36 píxeles fuera del contenedor por el lado derecho. Esto genera barras de desplazamiento horizontal indeseadas en la ventana del navegador, rompe la alineación de las columnas y deforma los márgenes.

Escenario B: Bajo box-sizing: border-box (Estándar profesional)#

Bajo border-box, width: 100% le ordena al motor: "Haz que la caja completa, de borde exterior a borde exterior, mida el 100% del padre (500 px)". El padding y los bordes se acomodan hacia adentro, encogiendo el área de contenido:

begin{aligned} W_{total} &= width = 500 px W_{contenido} &= W_{total} - (padding + borde) W_{contenido} &= 500 px - (32 px + 4 px) = 464 px end{aligned}

Consecuencia geométrica:

Desbordamiento (Overflow) = 500 px - 500 px = 0 px

La caja encaja milimétricamente en su contenedor sin importar cuánto padding o borde se agregue.


3. Accesibilidad de errores: Por qué el borde rojo viola WCAG 1.4.1#

Un error frecuente al validar formularios consiste en cambiar el borde del campo a color rojo cuando los datos son inválidos:

CSS
/* Antipatrón accesible */
.input-invalid {
  border-color: #ff0000;
}

Este patrón falla catastróficamente por dos razones fundamentales:

  1. *Violación del Criterio de Éxito WCAG 1.4.1 (Use of Color - Nivel A): - El color no debe utilizarse como el único* medio visual para transmitir información, indicar una acción, solicitar una respuesta o distinguir un elemento visual. - Las personas con deficiencias en la percepción del color (como protanopía o deuteranopía, que afectan a aproximadamente el 8% de los varones) perciben el rojo y el gris/verde con tonalidades y contrastes casi idénticos. Para ellos, el borde "rojo" es visualmente indistinguible del borde normal.
  2. Invisibilidad total para tecnologías de asistencia: - Un lector de pantalla no lee el valor hexadecimal de la propiedad CSS border-color. - Cuando el usuario ciego se posiciona en el campo con el teclado, el lector de pantalla solo anuncia: "Task title, edit text". El usuario no tiene idea de que el envío falló, ni de por qué falló, ni de qué regla se infringió.

La solución arquitectónica con WAI-ARIA y texto descriptivo:#

DIAGRAMA MERMAID
flowchart LR
    A["Usuario pulsa Submit con campo vacío"] --> B["Navegador o Script detecta error"]
    B --> C["Aplica aria-invalid='true'"]
    B --> D["Renderiza mensaje <p id='error-id' role='alert'>"]
    B --> E["Asocia aria-describedby='error-id'"]
    C & D & E --> F["Lector de pantalla anuncia:<br/>'Task title, invalid entry. Title cannot be empty.'"]
  1. Texto explícito visible: El mensaje de error debe existir como texto claro y comprensible (ej. "Title cannot be empty"), acompañado si es posible de un icono de advertencia.
  2. aria-invalid="true": Al colocarse en el <input>, el Árbol de Accesibilidad marca el nodo con el estado Invalid. El lector de pantalla añade inmediatamente la palabra "Entrada no válida" al anunciar el campo.
  3. aria-describedby="title-error": Conecta el campo con el identificador del párrafo de error. Cuando el campo recibe el foco, el lector anuncia la descripción asociada.
  4. role="alert" en el contenedor del error: Si el mensaje aparece de forma dinámica tras una validación asíncrona, role="alert" garantiza que el sintetizador de voz interrumpa cortésmente para leer el mensaje sin forzar al usuario a mover el foco manualmente.

Diagrama#

DIAGRAMA MERMAID
flowchart TD
    A[HTML con semántica] --> B[Controles por teclado]
    B --> C[Etiquetas y mensajes asociados]
    A --> D[CSS aplica cajas y flujo]
    D --> E[Inspección de tamaño y foco]
    C --> F[Usuario comprende y opera]
    E --> F

Diagrama estático del tema
Diagrama estático del tema
Fuente editable: Mermaid.

La presentación complementa el orden semántico; no lo reemplaza.


Ejemplo razonado#

Este es un documento HTML completo y autónomo que puedes guardar como index.html y abrir directamente en tu navegador:

HTML
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Task Management · Accessible Form</title>
    <style>
      /* 1. Global Box-Sizing Reset */
      *, *::before, *::after {
        box-sizing: border-box;
      }

      body {
        margin: 0;
        padding: 2rem 1rem;
        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        line-height: 1.5;
        color: #1a1a1a;
        background-color: #f7f9fa;
      }

      /* 2. Structured Layout */
      main {
        max-width: 32rem;
        margin: 0 auto;
        padding: 2rem;
        background: #ffffff;
        border-radius: 8px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
      }

      h1 {
        margin-top: 0;
        margin-bottom: 1.5rem;
        font-size: 1.5rem;
      }

      form {
        display: grid;
        grid-template-columns: 1fr;
        gap: 1.25rem;
      }

      .field-group {
        display: grid;
        grid-template-columns: 1fr;
        gap: 0.35rem;
      }

      label {
        font-weight: 600;
        font-size: 0.95rem;
      }

      .field-hint {
        margin: 0;
        font-size: 0.85rem;
        color: #555555;
      }

      input[type="text"] {
        width: 100%;
        padding: 0.75rem;
        font-size: 1rem;
        border: 1px solid #767676; /* WCAG AA 4.5:1 boundary contrast */
        border-radius: 4px;
        background: #ffffff;
      }

      /* 3. Accessible Focus Indicators */
      input:focus-visible,
      button:focus-visible {
        outline: 3px solid #005fcc;
        outline-offset: 2px;
      }

      /* 4. Semantic Error Presentation */
      input[aria-invalid="true"] {
        border-color: #c92a2a;
        border-width: 2px;
      }

      .field-error {
        margin: 0;
        font-size: 0.85rem;
        font-weight: 600;
        color: #c92a2a;
      }

      /* 5. Native Accessible Button */
      button[type="submit"] {
        padding: 0.75rem 1.5rem;
        font-size: 1rem;
        font-weight: 600;
        color: #ffffff;
        background-color: #0b7285;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        justify-self: start;
      }

      button[type="submit"]:hover {
        background-color: #085a6a;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Add a new task</h1>
      <form action="/tasks" method="post" novalidate>
        <div class="field-group">
          <label for="task-title">Title (required)</label>
          <input 
            id="task-title" 
            name="title" 
            type="text" 
            required
            aria-describedby="title-help title-error"
            aria-invalid="true"
          >
          <p id="title-help" class="field-hint">Enter a descriptive title for this activity.</p>
          <p id="title-error" class="field-error" role="alert">Error: Title cannot be empty.</p>
        </div>

        <button type="submit">Save task</button>
      </form>
    </main>
  </body>
</html>

Por qué esta estructura es robusta:#

  • <main> define el hito semántico primario para lectores de pantalla.
  • <label for="task-title"> está explícitamente vinculado al id="task-title".
  • aria-describedby="title-help title-error" enlaza tanto la instrucción complementaria como el mensaje de error activo en el AOM.
  • aria-invalid="true" informa programáticamente del estado de error a cualquier tecnología de apoyo.
  • :focus-visible proporciona un anillo de foco nítido y de alto contraste sin afear la experiencia para usuarios de ratón.
  • box-sizing: border-box garantiza que el padding: 0.75rem no genere desbordamiento exterior.

Práctica independiente y Checklist de auditoría a11y / DevTools#

Crea un archivo local task-form.html y amplíalo para gestionar dos campos: Título de la tarea (obligatorio) y Fecha límite (opcional, <input type="date">).

Checklist paso a paso de auditoría con DevTools y teclado:#

Antes de dar por terminado tu formulario, ejecuta de manera estricta esta matriz de verificación:

#Prueba / AuditoríaProcedimiento operativoResultado esperado
1Navegación exclusiva por tecladoDesconecta o ignora el ratón. Presiona la tecla Tab repetidamente desde la barra de direcciones.El foco avanza secuencialmente: Campo Título -> Campo Fecha -> Botón Guardar. Shift + Tab retrocede en el orden inverso exacto.
2Visibilidad del foco de tecladoObserva cada elemento interactivo mientras tiene el foco activo.Un anillo visual de foco evidente (mínimo 2px de grosor y alto contraste) rodea cada control. En ningún momento desaparece el foco visual.
3Activación de controlesCon el foco sobre el botón Guardar, presiona Enter y luego Space.Ambos comandos activan el botón y disparan el intento de envío del formulario.
4Inspección del Árbol de Accesibilidad (AOM)Abre DevTools (F12) -> Pestaña Elements -> Subpestaña Accessibility (Accesibilidad). Selecciona el <input>.El nodo reporta: Role: textbox, Name: "Title (required)", Description: "...", Invalid: true.
5Área táctil de la etiquetaHaz clic con el ratón sobre el texto de la etiqueta "Title (required)".El cursor parpadeante de texto se ubica automáticamente dentro del <input> asociado.
6Inspección del Box ModelEn DevTools -> Pestaña Elements -> Subpestaña Computed (Estilos computados), inspecciona la caja del <input>.Verifica que el ancho total coincide exactamente con el ancho de la columna del padre y que no existe desplazamiento horizontal (overflow-x).
7Contraste de color (WCAG AA)En DevTools, haz clic en el cuentagotas de color del texto de ayuda o el botón.El ratio de contraste contra el fondo blanco debe ser de al menos 4.5:1 para texto normal y 3:1 para bordes de controles.

Registra tus predicciones y observaciones antes de consultar la guía de soluciones de frontend.


Preguntas de comprensión profunda y diagnóstico socrático#

Reflexiona sobre estas cinco preguntas de razonamiento técnico antes de avanzar:

Pregunta 1: La trampa del botón estilizado con <div>#

Un compañero de equipo escribe el siguiente código para un componente reutilizable:

HTML
<div class="custom-button" onclick="handleSubmit()">Submit Order</div>

Cuando un usuario invidente intenta utilizar la tienda, se queja de que no puede tramitar su pedido. El desarrollador responde: "En mi pantalla se ve idéntico a un botón y al hacer clic funciona perfectamente".

  • ¿Qué información reporta el Árbol de Accesibilidad (AOM) al sistema operativo para este elemento?
  • ¿Por qué un usuario que navega exclusivamente con la tecla Tab jamás llegará a este elemento?
  • Si el desarrollador agrega tabindex="0", ¿por qué el usuario aún no podrá activar el botón usando la barra espaciadora como espera en un botón estándar? ¿Qué evento de la ventana se disparará en su lugar?

Pregunta 2: El colapso del placeholder como única etiqueta#

Un diseñador propone eliminar todos los <label> del formulario y colocar únicamente placeholder="Enter your title here..." dentro de los inputs para "ahorrar espacio vertical y lograr un diseño más limpio".

  • Explica qué ocurre con la memoria de trabajo del usuario cuando comienza a escribir texto dentro del campo y el placeholder desaparece. ¿Cómo afecta esto a personas con discapacidades cognitivas o de atención?
  • Si el usuario borra el campo para corregirlo, ¿cómo sabe qué formato esperaba el sistema?
  • ¿Por qué muchos lectores de pantalla no calculan el placeholder como el nombre accesible canónico del control?

Pregunta 3: Diagnóstico geométrico de desbordamiento horizontal#

Un desarrollador nota que en pantallas móviles aparece una barra de desplazamiento horizontal no deseada de 32 píxeles. Encuentra la regla responsable:

CSS
.card {
  width: 100vw;
  padding: 16px;
  background: white;
}

Para solucionarlo rápidamente sin investigar, el desarrollador agrega overflow-x: hidden en el <body>.

  • ¿Por qué overflow-x: hidden es un parche cosmético que oculta el problema en lugar de resolverlo? ¿Qué información visual o interacción de foco puede quedar cortada fuera de la pantalla?
  • Utilizando la fórmula matemática del modelo de caja con content-box y 100vw, explica exactamente de dónde provienen esos 32 píxeles extra de desbordamiento.
  • ¿Cómo se resuelve este problema arquitectónicamente mediante box-sizing: border-box y max-width: 100%?

Pregunta 4: Cumplimiento de WCAG 1.4.1 y arquitectura de estados de error#

Observa este diseño de error:

HTML
<input id="card-num" style="border: 2px solid red;">
  • ¿Por qué este campo es inaccesible para una persona con protanopía (ceguera al color rojo)?
  • Diseña la tríada completa requerida (HTML semántico, atributo ARIA de estado y atributo ARIA de descripción) para que este error sea perceptible tanto visual como auditivamente.

Pregunta 5: La ergonomía visual de :focus vs :focus-visible#

  • ¿Cuál es la diferencia de comportamiento entre la pseudo-clase :focus y la pseudo-clase :focus-visible?
  • ¿Por qué los diseñadores solían exigir históricamente outline: none en los botones?
  • ¿Cómo resuelve :focus-visible el conflicto entre la estética deseada por el diseñador al hacer clic con el ratón y la necesidad crítica de orientación del usuario que navega con el teclado?

Code smell#

Síntoma: Sustitución de controles interactivos nativos por <div> genéricos (Divitis interactiva)#

Un equipo de frontend construye una tabla editable donde cada acción es un div:

HTML
<div class="btn-delete" onclick="deleteRow(42)">Delete</div>
  • Evidencia observable: El elemento no puede enfocarse con la tecla Tab, la tecla Enter/Espacio no tiene efecto, las herramientas de auditoría de accesibilidad (Lighthouse, axe-core) reportan violaciones graves de Elements must have interactive roles, y los lectores de pantalla anuncian simplemente "Delete, text".
  • Hipótesis del problema: Los desarrolladores asumieron que el CSS y los eventos de JavaScript son suficientes para definir la identidad de un control, ignorando el Árbol de Accesibilidad y los contratos del sistema operativo.
  • Consecuencia concreta: Exclusión total de usuarios de teclado, tecnología asistiva y personas mayores que dependen de navegación por voz o conmutadores.
  • Tratamiento mínimo justificado: Sustituir inmediatamente el <div> por un <button type="button">Delete</button>. Todo el comportamiento de teclado, foco y exposición semántica se recupera de inmediato con cero líneas de JavaScript adicional.

Criterio de salida#

Puedes considerar superada esta lección cuando:

  • Tu formulario cuenta con un hito semántico <main>, controles nativos vinculados explícitamente a sus <label> mediante for/id, y botón de envío formal.
  • Puedes navegar, operar y enviar el formulario utilizando exclusivamente el teclado (Tab, Shift+Tab, Enter, Espacio).
  • Todos los controles tienen un indicador visual claro mediante :focus-visible.
  • Los mensajes de error están expresados en texto claro, asociados programáticamente mediante aria-describedby y comunicados formalmente con aria-invalid="true".
  • Puedes demostrar matemáticamente la diferencia de cálculo entre content-box y border-box e inspeccionarla en la pestaña Computed de DevTools.
  • Tu código no utiliza <div> para simular botones ni outline: none sin reemplazo visible.

Lecturas oficiales#


Siguiente paso#

Continúa con 10 · TypeScript y el DOM: aprenderás a manipular estos controles en tiempo de ejecución sin confundir tipos estáticos con datos confiables de red o del documento.

Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)