UD3 · HTML5 Semántico, Estructura Accesible y Formularios
Presentación de clase · 45 diapositivas para seguir la unidad
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:
- Estructura Base: Define el esqueleto del documento con
<header>,<main>,<footer>y un bloque<nav>. - Maquetación del Producto: Utiliza las etiquetas
<article>o<section>de forma justificada para encapsular la ficha del producto. - Formulario de Reseñas: Construye un formulario de "Añadir reseña" integrando
<fieldset>,<legend>, etiquetas<label>vinculadas porfor/idy al menos 3 tipos deinputespecíficos (email,number,date, etc.). - 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). - 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.
- En la carpeta de la ficha de AURIA (la del ejercicio de la sección 5), crea el
compose.yamly elvite.config.mjsde la sección 6.2, instala Vite y Dart Sass condocker compose run, y crea la estructura de carpetasscss/de la sección 6.5. - 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. - Crea un mixin
flex-centery un componente_button.scssque lo use, con un hover calculado concolor.adjust. - Enlaza
/scss/main.scssen el HTML, arranca condocker compose upy abre http://localhost:5173. - Comprueba que, al cambiar solo
$blue-500en_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
@usey mixins), que se compila a un.cssnormal. - 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
- MDN Web Docs — HTML elements reference.
- W3C — Markup Validation Service.
- WebAIM — Creating Accessible Forms.
- Sass — Documentación oficial y guía del sistema de módulos
@use.