Saltar a contenido

UD3 · HTML5 Semántico, Estructura Accesible y Formularios

Presentación de clase · 45 diapositivas para seguir la unidad

Abrir o descargar el PDF

1. De Penpot a HTML: primer paso al código

flowchart TD
    A["<b>Mockup en Penpot</b><br/>(UD1 - UD2)"] --> B["<b>Estructura HTML</b><br/>(UD3)"]
    B --> C["<b>Estilos CSS</b><br/>(UD4 - UD5)"]
    C --> D["<b>Interactividad JS</b><br/>(UD7 - UD8)"]

El primer error habitual al pasar de diseño a código es maquetar pensando en "cómo se ve" en vez de "qué es". El HTML5 semántico obliga a hacerse la pregunta correcta: ¿qué tipo de contenido es esto?, no ¿qué aspecto tiene?


2. Estructura semántica del documento

2.1 Elementos de estructura de página

Elemento Propósito Puede repetirse
<header> Cabecera de la página o de una sección Sí (uno por <article>/<section> si aplica)
<nav> Bloque de navegación principal Sí, con moderación
<main> Contenido principal único de la página No — solo uno por documento
<article> Contenido autónomo y distribuible (post, noticia, comentario) Sí
<section> Agrupación temática con su propio encabezado Sí
<aside> Contenido relacionado pero secundario (sidebar, publicidad) Sí
<footer> Pie de página o de sección Sí
flowchart TD
    HTML["html (Documento)"] --> HEAD["head (Metadatos)"]
    HTML --> BODY["body (Contenido visible)"]

    subgraph Estructura_Body ["Estructura Semántica de Página"]
        BODY --> HEADER["header (Cabecera)"]
        BODY --> NAV["nav (Navegación)"]
        BODY --> MAIN["main (Contenido principal)"]
        BODY --> ASIDE["aside (Secundario / Sidebar)"]
        BODY --> FOOTER["footer (Pie)"]
    end

    subgraph Contenido_Main ["Dentro de main"]
        MAIN --> ART["article (Contenido autónomo)"]
        ART --> SEC["section (Bloque temático)"]
    end

Regla de decisión rápida

Si al extraer el elemento de su contexto (por ejemplo, mandarlo a un lector RSS) el contenido sigue teniendo sentido pleno por sí solo, es un <article>. Si necesita del contexto de la página para entenderse, es un <section> o un <div>.


2.2 <div>/<span> vs. elementos semánticos

Situación Elemento correcto Por qué
Agrupar visualmente sin significado propio <div> Es el contenedor neutro por definición
Resaltar texto en línea sin significado propio <span> Equivalente en línea de <div>
Un bloque que representa una noticia completa <article> Tiene significado semántico autónomo
Un bloque de navegación <nav> Los lectores de pantalla lo anuncian como punto de referencia (landmark)
Una cita textual de otra fuente <blockquote> Semántica específica, no un <div> con estilos de cita

El peligro de la 'divitis'

Usar <div> para todo no rompe la página visualmente, pero sí destruye la navegación por landmarks de los lectores de pantalla. Este es el motivo por el que RA2.a existe como criterio independiente. Lo evaluaremos en profundidad en la auditoría de accesibilidad del RA5 (UD9-UD10).

La accesibilidad también es ley

En España, el RD 1112/2018 obliga a que las webs y apps del sector público sean accesibles. Además, desde el 28 de junio de 2025 la Ley 11/2023 (que incorpora el Acta Europea de Accesibilidad, Directiva (UE) 2019/882) extiende la obligación a servicios privados como el comercio electrónico, la banca o el transporte, salvo las microempresas. Una tienda online como AURIA está, por tanto, obligada.


3. Jerarquía de encabezados y outline del documento

Los encabezados (<h1>-<h6>) no son selectores de tamaño visual: construyen la estructura jerárquica (outline) del documento que la tecnología asistiva navega como un índice.

flowchart TD
    H1["<b>h1</b> · Título principal de la página<br/>(único por documento)"] --> H2A["<b>h2</b> · Sección principal A"]
    H1 --> H2B["<b>h2</b> · Sección principal B"]

    H2A --> H3A["<b>h3</b> · Subsección A.1"]
    H2A --> H3B["<b>h3</b> · Subsección A.2"]

    H2B --> H3C["<b>h3</b> · Subsección B.1"]
Regla Correcto Incorrecto
Un único <h1> por página <h1>Catálogo de productos</h1> Varios <h1> compitiendo por ser el título principal
No saltar niveles h1 → h2 → h3 h1 → h3 (omite el nivel h2)
No elegir el nivel por tamaño visual <h2> aunque se deba ver pequeño Usar <h4> solo "porque se ve chico" (eso se estiliza con CSS)

4. Formularios web

4.1 Tipos de input más usados

Tipo Uso Ventaja sobre type="text" genérico
email Correo electrónico Valida formato y despliega teclado específico en móviles
tel Teléfono Despliega teclado numérico telefónico en móviles
number Valores numéricos Controles de incremento/decremento y validación de rango
date Fechas Selector de calendario nativo del sistema
url Direcciones web Valida sintaxis de URL
password Contraseñas Enmascara los caracteres introducidos
search Campos de búsqueda Semántica específica y botón de borrado rápido en navegadores
checkbox / radio Selección múltiple / única Comportamiento nativo accesible por teclado

4.2 Estructura accesible de un formulario

flowchart TD
    FORM["<b>form</b> (Contenedor global)"] --> FS["<b>fieldset</b> (Agrupador semántico)"]
    FS --> LEG["<b>legend</b>: Datos de contacto"]

    FS --> DIV1["<b>div</b> (Contenedor de campo)"]
    DIV1 --> LBL1["<b>label for='email'</b>"]
    DIV1 --> INP1["<b>input id='email' type='email'</b>"]

    FS --> DIV2["<b>div</b> (Contenedor de campo)"]
    DIV2 --> LBL2["<b>label for='tel'</b>"]
    DIV2 --> INP2["<b>input id='tel' type='tel'</b>"]
Elemento Función Por qué es obligatorio
<label for="id"> Vincula el texto descriptivo al campo Sin esta relación, un lector de pantalla no anuncia la función del campo al recibir el foco.
<fieldset> + <legend> Agrupa campos con un título temático Otorga contexto completo cuando hay múltiples inputs similares (ej. dirección de envío vs. facturación).
Atributo required Marca la obligatoriedad del campo Validación nativa accesible del navegador sin depender de JS.
Atributo aria-describedby Asocia mensajes de ayuda o error Anuncia las instrucciones de ayuda o alertas al situarse sobre el control.

4.3 Validación nativa vs. JavaScript

Validación Cuándo usarla Ejemplo
Nativa HTML5 (required, pattern, min, max) Reglas simples de formato, longitud o rango <input type="email" required>
JavaScript Reglas de negocio complejas (ej. "las contraseñas deben coincidir") Se trabajará en la UD8 (RA4, manipulación del DOM)

Validación en servidor

La validación nativa en cliente mejora la experiencia de usuario (UX), pero nunca sustituye a la validación en el backend. Todo dato debe ser revalidado en el servidor antes de ser procesado.


5. Ejercicio práctico guiado

Actividad de consolidación (no evaluable)

Objetivo: Traducir a HTML5 semántico y accesible la estructura de interfaz diseñada previamente en Penpot (UD1-UD2).

Pasos recomendados para practicar:

  1. Estructura Base: Define el esqueleto del documento con <header>, <main>, <footer> y un bloque <nav>.
  2. Maquetación del Producto: Utiliza las etiquetas <article> o <section> de forma justificada para encapsular la ficha del producto.
  3. Formulario de Reseñas: Construye un formulario de "Añadir reseña" integrando <fieldset>, <legend>, etiquetas <label> vinculadas por for/id y al menos 3 tipos de input específicos (email, number, date, etc.).
  4. Auditoría de Outline: Comprueba el árbol de encabezados con la extensión HeadingsMap para garantizar que no existan saltos de nivel (h1 \(\rightarrow\) h3).
  5. Validación: Comprueba la sintaxis de tu código con la herramienta oficial W3C Markup Validator.

6. Preparando los estilos: introducción a SASS

Con la estructura HTML terminada, el siguiente paso es darle estilo. Antes de escribir CSS en la UD4 vamos a preparar dónde y cómo lo vamos a escribir, porque un proyecto real no tiene un único styles.css de 2.000 líneas: tiene los estilos organizados en piezas pequeñas, con nombres y valores reutilizables. Para eso usamos SASS.

6.1 ¿Qué es SASS y por qué existe?

SASS es un preprocesador de CSS: un lenguaje que se parece muchísimo a CSS, pero con herramientas extra (variables, anidamiento, mixins, módulos). El navegador no entiende SASS. Lo que escribes en ficheros .scss pasa por un compilador que lo convierte en un .css normal, y es ese .css el que recibe el navegador. En nuestros proyectos ese trabajo lo hace Vite, que lleva dentro el compilador oficial (Dart Sass) y se ejecuta en un contenedor Docker.

flowchart LR
    A["<b>Ficheros .scss</b><br/>(lo que escribes tú)"] -->|"compila<br/>(Vite + Dart Sass)"| B["<b>CSS</b><br/>(lo que lee el navegador)"]
    B --> C["<b>Navegador</b><br/>http://localhost:5173"]

Analogía: la receta y el plato

El .scss es tu receta, escrita con abreviaturas que solo entiendes tú en la cocina ("salsa base ×2"). El CSS es el plato servido: el cliente (el navegador) nunca ve la receta, solo el resultado. Por eso nunca se edita el CSS generado a mano (el de la carpeta dist/): la próxima vez que cocines (compiles), se sobrescribe y pierdes el cambio.

SASS tiene dos sintaxis. Usaremos SCSS (extensión .scss), que es CSS válido con extras: cualquier CSS que ya sepas escribir funciona tal cual dentro de un .scss.

6.2 Instalar y compilar: Vite + Sass en Docker

La implementación oficial y actual de SASS es Dart Sass (el antiguo Node Sass está abandonado). No la usamos sola: la usamos a través de Vite, un servidor de desarrollo que compila el .scss cada vez que guardas y actualiza el navegador sin recargar. Y todo eso corre en un contenedor Docker: no hace falta instalar Node.js en tu equipo, solo Docker.

1. El entorno: compose.yaml en la raíz del proyecto.

# compose.yaml · el entorno de trabajo (Node, npm, Vite y Sass viven aquí dentro)
services:
  web:
    image: node:22-alpine     # imagen oficial de Node.js 22
    user: node                # los ficheros que cree el contenedor serán tuyos, no de root
    working_dir: /app
    volumes:
      - .:/app                # tu carpeta del proyecto, dentro del contenedor
    ports:
      - "5173:5173"           # http://localhost:5173 → Vite
    command: npx vite --host 0.0.0.0

2. Instalar y arrancar, desde la carpeta del proyecto:

# 1. Ficha del proyecto (solo la primera vez)
docker compose run --rm web npm init -y
# 2. Vite y Dart Sass como herramientas de desarrollo
docker compose run --rm web npm install -D vite sass
# 3. Arrancar Vite y dejarlo vigilando
docker compose up

docker compose run --rm web … ejecuta un comando suelto dentro del contenedor y lo borra al terminar (--rm). Los ficheros que crea (package.json, node_modules/) aparecen en tu carpeta gracias al volumen. docker compose up arranca el servicio con su command: Vite. Para pararlo, Ctrl+C (o docker compose down).

3. Enlazar el .scss en el HTML. Con Vite se enlaza directamente el .scss: Vite lo compila al servirlo.

<!-- index.html -->
<link rel="stylesheet" href="/scss/main.scss">

Abre http://localhost:5173. Cada vez que guardas un .scss, Vite lo recompila y el navegador aplica el CSS nuevo sin recargar la página (HMR). Si hay un error de SASS, aparece en la terminal de docker compose up y en el propio navegador, con fichero y línea.

4. Source maps. Para que DevTools te diga en qué línea de qué .scss está cada regla (y no en el CSS compilado), actívalos en vite.config.mjs:

// vite.config.mjs
import { defineConfig } from 'vite';

export default defineConfig({
  css: {
    devSourcemap: true,   // F12 muestra el .scss y la línea de cada regla
  },
  // server: { watch: { usePolling: true } },  // solo si al guardar no se actualiza (Docker Desktop en Windows)
});

5. La versión final: build. Cuando quieras la web lista para publicar:

docker compose run --rm web npx vite build

Genera la carpeta dist/ con el HTML y un único CSS compilado y comprimido (dist/assets/index-….css). Ese es el CSS "de verdad" que se publica, y no se edita a mano: se vuelve a generar en cada build.

¿Y si solo quiero compilar, sin Vite?

El mismo contenedor sirve para usar el compilador de Sass a pelo: docker compose run --rm web npx sass --watch scss/main.scss css/main.css genera css/main.css cada vez que guardas, y entonces en el HTML se enlaza ese .css. Es útil para entender qué hace Vite por dentro, pero en clase trabajamos con Vite.

6.3 Variables: los tokens de la UD2, en código

En la UD2 definisteis los tokens de AURIA en Penpot. En SASS, un token global es una variable que empieza por $:

// _tokens.scss
$blue-500: #1E88E5;          // Nivel 1 · Global
$space-md: 16px;
$radius-md: 8px;

$color-brand-primary: $blue-500;   // Nivel 2 · Semántico

Si mañana cambia el azul de marca, cambias una línea y al compilar se actualiza en todos los sitios donde se usa. Es exactamente la idea de los tres niveles de la UD2.

¿Variables SASS o variables CSS (--color)?

Las dos existen y no se sustituyen, se complementan:

Variable SASS ($color) Custom property CSS (--color)
Cuándo existe Solo al compilar; en el .css final desaparece y queda el valor En el navegador, mientras la página está abierta
¿Puede cambiar en vivo? No Sí (modo oscuro, JavaScript, media queries)
Uso típico Tokens globales, breakpoints, cálculos Tokens semánticos que cambian con el tema

Una práctica muy habitual es combinarlas: los valores en SASS y los tokens semánticos publicados como custom properties.

:root {
  --color-brand-primary: #{$blue-500};
}

6.4 Anidamiento: el CSS sigue la forma del HTML

SASS permite escribir unas reglas dentro de otras, siguiendo la estructura del HTML. El símbolo & significa "el selector en el que estoy":

.card {
  padding: $space-md;
  border-radius: $radius-md;

  &:hover {                 // → .card:hover
    box-shadow: 0 2px 8px rgb(0 0 0 / 0.2);
  }

  &__title {                // → .card__title  (nombre BEM, UD5)
    font-size: 1.25rem;
  }
}
.card {
  padding: 16px;
  border-radius: 8px;
}
.card:hover {
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.2);
}
.card__title {
  font-size: 1.25rem;
}

La regla de los 3 niveles

Anidar es cómodo, pero no es gratis. Cada nivel añade un selector más al CSS final:

.page { main { article { section { h2 { color: red; } } } } }
// → .page main article section h2 { color: red; }

Ese selector es larguísimo, muy específico (difícil de sobrescribir después) y se rompe en cuanto cambias el HTML. Regla práctica: no más de 3 niveles, y anida sobre todo &:hover, &:focus, &__elemento y &--modificador.

6.5 Parciales y módulos: un fichero por pieza

Un parcial es un fichero .scss cuyo nombre empieza por _ (por ejemplo _tokens.scss). El guion bajo le dice al compilador "este fichero no se compila solo; es una pieza de otro". Las piezas se cargan con @use:

proyecto/
├── compose.yaml          ← el entorno Docker
├── package.json          ← creado con npm init (dependencias: vite, sass)
├── vite.config.mjs       ← source maps
├── index.html            ← <link rel="stylesheet" href="/scss/main.scss">
├── node_modules/         ← generado por npm install, NO se toca
├── dist/                 ← generado por vite build, NO se toca
└── scss/
    ├── _tokens.scss      ← colores, espacios, radios (UD2)
    ├── _mixins.scss      ← mixins reutilizables
    ├── _base.scss        ← estilos de body, h1-h6, enlaces
    ├── components/
    │   ├── _button.scss
    │   └── _card.scss
    └── main.scss         ← solo importa las piezas
// main.scss — el índice: no lleva estilos propios
@use 'base';
@use 'components/button';
@use 'components/card';
// components/_card.scss — cada pieza carga lo que necesita
@use '../tokens' as *;

.card {
  padding: $space-md;
}

@use en lugar de @import

En tutoriales antiguos verás @import. Está obsoleto desde Dart Sass 1.80 y se eliminará en Dart Sass 3.0. Con @use, cada fichero declara qué necesita, y las variables no se mezclan entre ficheros sin querer. Sin as *, se usan con prefijo: tokens.$space-md.

6.6 Mixins: bloques de estilo reutilizables

Un mixin es un bloque de declaraciones con nombre que puedes "pegar" donde quieras con @include. Puede recibir parámetros, como una función:

// _mixins.scss · los @use siempre van al principio del fichero
@use 'tokens' as *;

@mixin flex-center($gap: 0) {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: $gap;
}

@mixin focus-visible {
  &:focus-visible {
    outline: 3px solid $color-brand-primary;
    outline-offset: 2px;
  }
}
// components/_button.scss
@use '../mixins' as *;
@use '../tokens' as *;

.button {
  @include flex-center($space-md);
  @include focus-visible;   // foco visible y accesible en todos los botones
}

Variable, mixin… ¿cuál uso?

  • Si repites un valor (un color, un tamaño) → variable.
  • Si repites un grupo de declaraciones (las 4 líneas para centrar con Flexbox) → mixin.
  • Si no repites nada → CSS normal. No conviertas en mixin algo que usas una sola vez.

6.7 Funciones: calcular en lugar de copiar

SASS trae módulos de funciones listos para usar. El más útil al principio es sass:color, para obtener variantes de un color sin inventarse otro código hexadecimal:

@use 'sass:color';
@use '../tokens' as *;

.button {
  background: $color-brand-primary;

  &:hover {
    background: color.adjust($color-brand-primary, $lightness: -10%);  // 10 % más oscuro
  }
}

Así el color de hover depende del color de marca: si cambia el azul, el hover se recalcula solo.

6.8 Errores frecuentes con SASS

Error Consecuencia Solución
Editar el CSS generado (dist/) a mano Se pierde en el siguiente build Edita siempre el .scss
Abrir index.html con doble clic (o con Live Server) Nadie compila el .scss: la página sale sin estilos Abre http://localhost:5173, la dirección de Vite
Anidar 5 o 6 niveles Selectores enormes y frágiles Máximo 3 niveles
Rutas mal en @use al mover parciales a carpetas Error de compilación "Can't find stylesheet to import" La ruta es relativa al fichero que hace el @use (../tokens)
Usar @import Aviso de obsoleto; dejará de funcionar @use
Contenedor parado "He cambiado el .scss y no se ve nada" docker compose up y deja esa terminal abierta: los errores salen ahí
Puerto 5173 ocupado docker compose up falla con port is already allocated Para el otro proyecto (docker compose down en su carpeta)

6.9 Ejercicio: preparar la arquitectura de estilos de AURIA

Actividad de consolidación (no evaluable)

Objetivo: dejar lista la estructura SASS sobre la que escribiremos los estilos de la ficha de AURIA en la UD4.

  1. En la carpeta de la ficha de AURIA (la del ejercicio de la sección 5), crea el compose.yaml y el vite.config.mjs de la sección 6.2, instala Vite y Dart Sass con docker compose run, y crea la estructura de carpetas scss/ de la sección 6.5.
  2. En _tokens.scss, pasa a variables SASS los tokens de color y espaciado que definiste en Penpot en la UD2, respetando los niveles global y semántico.
  3. Crea un mixin flex-center y un componente _button.scss que lo use, con un hover calculado con color.adjust.
  4. Enlaza /scss/main.scss en el HTML, arranca con docker compose up y abre http://localhost:5173.
  5. Comprueba que, al cambiar solo $blue-500 en _tokens.scss, cambian el botón, su hover y el contorno de foco.

7. Glosario

Término Definición
HTML semántico Elegir cada etiqueta por lo que el contenido es, no por cómo se ve.
Divitis Usar <div> para todo, aunque exista una etiqueta con significado.
Landmark Zona de la página que el lector de pantalla anuncia y a la que se puede saltar.
Rol ARIA Lo que un elemento «es» para la accesibilidad: <nav> tiene el rol navigation.
Árbol de accesibilidad Versión de la página que el navegador entrega al lector de pantalla (DevTools → Accessibility).
Nombre accesible Texto con el que se anuncia un elemento; en un campo, el de su <label>.
Lector de pantalla Programa que lee la página en voz alta: NVDA, JAWS, VoiceOver, TalkBack.
Outline Índice de la página formado por los encabezados h1–h6.
for / id Unen un <label> con su campo: el mismo valor en los dos atributos.
fieldset / legend Agrupan campos relacionados y les dan un título común.
aria-describedby Asocia a un campo su texto de ayuda o de error.
Validación nativa Comprobaciones que hace el navegador con atributos (required, type, min, pattern…), sin JavaScript.
pattern Expresión regular que debe cumplir todo el valor del campo.
Validador W3C Comprueba que el HTML cumple la especificación (no comprueba la accesibilidad).
Término Definición
Preprocesador Lenguaje que se traduce a CSS antes de llegar al navegador (SASS, Less, Stylus).
SCSS Sintaxis de SASS con llaves y punto y coma; todo CSS válido es SCSS válido.
Compilar Traducir los .scss a un .css que entiende el navegador.
Dart Sass Compilador oficial (paquete sass de npm). Node Sass está abandonado.
Vite Servidor de desarrollo: compila el .scss al guardar y actualiza el navegador sin recargar (HMR). vite build genera la versión final en dist/.
Docker / contenedor Entorno aislado con todo lo necesario (Node, npm, Vite, Sass) sin instalarlo en tu equipo. Se describe en compose.yaml y se arranca con docker compose up.
Volumen Carpeta de tu equipo montada dentro del contenedor (.:/app): lo que guardas en VS Code lo ve Vite al instante.
Source map Fichero .map que enlaza el CSS compilado con su .scss para las DevTools.
Variable $ Valor reutilizable que solo existe al compilar.
Custom property -- Variable de CSS que vive en el navegador y puede cambiar en vivo.
& Selector padre: «el selector en el que estoy» (&:hover → .card:hover).
Parcial _ Fichero que no se compila solo; se carga desde otro.
@use Carga un módulo una sola vez, con espacio de nombres (tokens.$space-md, o sin él con as *).
@mixin / @include Definen un bloque de declaraciones reutilizable / lo insertan.
Función Devuelve un valor: color.adjust, math.div o una propia con @function.
@import Sistema antiguo, obsoleto: se eliminará en Dart Sass 3.0. Usa @use.

8. Resumen y conexión con la siguiente unidad

  • El HTML5 semántico transmite intención técnica a los motores de búsqueda y tecnologías de asistencia.
  • La accesibilidad en formularios no es un añadido estético: comienza por la correcta vinculación técnica entre <label> y sus <input>.
  • SASS no es otro CSS: es una herramienta para escribir y organizar el CSS (variables, anidamiento moderado, parciales con @use y mixins), que se compila a un .css normal.
  • Con la estructura semántica construida y los estilos organizados, en la UD4 aplicaremos la capa visual mediante CSS3: cascada, especificidad y layouts avanzados (Flexbox y Grid).
flowchart LR
    UD3["UD3<br/>HTML5 semántico"] --> UD4["UD4<br/>CSS3 moderno<br/>Flexbox/Grid"]

9. Referencias y fuentes