A 04-01 vam veure que els controls natius de formulari (<input>, <select>, <textarea>, <button>) porten rol, focus i operació per teclat de fàbrica. Però un formulari accessible necessita quelcom més que bons controls: necessita que cada control tingui nom, que els camps relacionats estiguin agrupats, que les ajudes i els errors arribin a tothom i que l'ordre de focus sigui lògic. Aquests són precisament els criteris 3.3.x ("Ajuda a l'entrada de dades") que al Mòdul 3 vam estudiar com a requisit i que ara implementem.

El terreny de pràctica és el formulari de matrícula/registre de Cursalia: nom, correu, telèfon, contrasenya, nivell d'estudis, interessos i acceptació de condicions. L'anirem construint peça a peça, sempre amb exemples correctes davant d'incorrectes, pensant en la Marta (NVDA li ha de llegir l'etiqueta de cada camp), l'Hugo (necessita instruccions clares i sense ambigüitat) i la Lucía (el recorre sencer amb el teclat).

Contingut

  1. L'etiqueta: associar label i control
  2. Per què el placeholder no és una etiqueta
  3. Agrupar amb fieldset i legend
  4. Camps obligatoris: required i aria-required
  5. Instruccions i ajudes amb aria-describedby
  6. Errors accessibles (i una primera ullada a role="alert")
  7. autocomplete i tipus d'input
  8. Focus i ordre lògic
  9. Errors comuns i consells
  10. Exercicis
  11. Conclusió

L'etiqueta: associar label i control

Tot control de formulari ha de tenir una etiqueta accessible. L'etiqueta és el name del control (recorda 4.1.2): sense ella, la Marta sent "quadre d'edició" sense saber què escriure. Hi ha dues maneres correctes d'associar un <label> amb el seu control.

Opció A — for / id (la més comuna i flexible): l'atribut for del label apunta a l'id del control.

<!-- CORRECTE: for apunta a l'id -->
<label for="email">Correu electrònic</label>
<input type="email" id="email" name="email">

Opció B — label envoltant: el control va dins del <label>. No cal for/id.

<!-- CORRECTE: el label envolta el control -->
<label>
  Correu electrònic
  <input type="email" name="email">
</label>

Totes dues fan que en prémer sobre el text s'enfoqui el camp (àrea de clic més gran, cosa que agraeix la Lucía) i que el lector l'anunciï amb el seu nom. L'error habitual és no associar-los: text solt al costat d'un input.

<!-- INCORRECTE: el text no està associat al camp -->
<span>Correu electrònic</span>
<input type="email" name="email">

Aquí no hi ha <label> (o el seu for no coincideix amb cap id): la Marta sent "quadre d'edició en blanc". Si per disseny no pots mostrar un text visible (per exemple, el cercador del catàleg amb només una lupa), utilitza aria-label, que veurem a 04-03:

<input type="search" aria-label="Cerca cursos">

Per què el placeholder no és una etiqueta

És temptador posar només un placeholder="Correu electrònic" i estalviar-se el <label>. És un error seriós per diversos motius:

Problema del placeholder com a etiqueta A qui afecta
Desapareix en començar a escriure; ja no saps què era aquell camp Hugo (memòria/atenció), tothom
El seu contrast sol ser baix (gris clar) i no compleix 1.4.3 Sofía (baixa visió)
Molts lectors no l'anuncien com a nom fiable Marta
No es pot prémer per enfocar el camp Lucía
<!-- INCORRECTE: només placeholder, sense label -->
<input type="email" placeholder="Correu electrònic">

<!-- CORRECTE: label visible; el placeholder, si de cas, només com a exemple de format -->
<label for="email">Correu electrònic</label>
<input type="email" id="email" placeholder="[email protected]">

Regla: el placeholder és, com a molt, una pista de format; mai substitueix l'etiqueta.

Agrupar amb fieldset i legend

Quan diversos controls formen un sol grup conceptual —un conjunt de radios (tria'n un) o de checkboxes (tria'n diversos)—, cal envoltar-los en <fieldset> i donar-los un títol comú amb <legend>. Així el lector anuncia el grup ("Nivell d'estudis, grup") abans de cada opció, donant el context que un radio solt no tindria.

<!-- CORRECTE: grup de radios amb fieldset/legend -->
<fieldset>
  <legend>Nivell d'estudis</legend>

  <label><input type="radio" name="nivel" value="secundaria"> Secundària</label>
  <label><input type="radio" name="nivel" value="grado"> Grau universitari</label>
  <label><input type="radio" name="nivel" value="master"> Màster o superior</label>
</fieldset>

La Marta sent: "Nivell d'estudis, grup. Secundària, botó de radio, 1 de 3". Sense el fieldset, sentiria tres radios solts sense saber a quina pregunta responen.

<!-- INCORRECTE: un text solt no agrupa res semànticament -->
<p>Nivell d'estudis</p>
<label><input type="radio" name="nivel" value="grado"> Grau</label>

El mateix aplica als interessos (checkboxes) o a l'acceptació de diverses condicions. No abusis del fieldset per a camps independents (nom, correu): és només per a grups.

Camps obligatoris: required i aria-required

Marcar un camp com a obligatori té dues cares que cal cobrir alhora: la funcional i la comunicativa.

  • required: atribut natiu. Activa la validació del navegador i exposa l'estat "requerit" al lector (la Marta sent "requerit"). És la primera opció.
  • aria-required="true": només si necessites comunicar l'obligatorietat sense activar la validació nativa del navegador. No dupliquis tots dos sense motiu; amb required normalment n'hi ha prou.
  • Indicació visual i textual: no n'hi ha prou amb un asterisc vermell (falla 1.4.1, "no només per color/forma"). Explica què significa l'asterisc i, encara millor, reforça'l amb text.
<!-- CORRECTE: obligatori comunicat de tres maneres: nativa, visual i textual -->
<p id="leyenda-oblig">Els camps marcats amb * són obligatoris.</p>

<label for="nombre">Nom complet *</label>
<input type="text" id="nombre" name="nombre" required
       aria-describedby="leyenda-oblig">
<!-- INCORRECTE: l'obligatorietat viu només al color de l'asterisc -->
<label for="nombre">Nom complet <span style="color:red">*</span></label>
<input type="text" id="nombre">   <!-- ni required, ni explicació del * -->

Instruccions i ajudes amb aria-describedby

Molts camps necessiten una ajuda: format de la contrasenya, per a què s'usa el telèfon, etc. Perquè aquesta ajuda es llegeixi juntament amb el camp (i no quedi com a text orfe al costat), s'associa amb aria-describedby, que apunta a l'id del text d'ajuda. El lector llegeix primer l'etiqueta i després la descripció.

<!-- CORRECTE: l'ajuda s'associa amb aria-describedby -->
<label for="pass">Contrasenya</label>
<input type="password" id="pass" name="pass"
       aria-describedby="ayuda-pass" required>
<p id="ayuda-pass">Mínim 8 caràcters, amb una lletra i un número.</p>

La Marta sent: "Contrasenya, quadre d'edició, requerit. Mínim 8 caràcters, amb una lletra i un número". L'Hugo llegeix aquesta mateixa instrucció clara abans d'escriure, no després de fallar. aria-describedby pot apuntar a diversos ids separats per espais (per exemple, l'ajuda i el missatge d'error del mateix camp).

<!-- INCORRECTE: l'ajuda existeix visualment però no està associada -->
<label for="pass">Contrasenya</label>
<input type="password" id="pass">
<p>Mínim 8 caràcters, amb una lletra i un número.</p>  <!-- sense aria-describedby: el lector no la vincula -->

Errors accessibles (i una primera ullada a role="alert")

Quan l'usuari s'equivoca, el criteri 3.3.1 exigeix identificar l'error en text (no només amb color) i descriure'l de manera útil. Bones pràctiques:

  • Text explícit, no només una vora vermella: "El correu no és vàlid: falta la @".
  • Associat al camp amb aria-describedby, perquè el lector el llegeixi en enfocar aquell camp.
  • No dependre del color: afegeix una icona o la paraula "Error" (això és 1.4.1, que veurem a fons a 04-04).
  • Moure el focus al primer camp amb error o resumir els errors al principi ajuda la Lucía i la Marta.
<!-- CORRECTE: error en text, associat, i no només per color -->
<label for="email">Correu electrònic *</label>
<input type="email" id="email" name="email" required
       aria-describedby="err-email" aria-invalid="true">
<p id="err-email" class="error">
  <span aria-hidden="true">⚠️</span> Error: el correu ha d'incloure una @.
</p>

Aquí aria-invalid="true" marca el camp com a invàlid a l'arbre d'accessibilitat, i aria-describedby fa que el missatge es llegeixi en enfocar-lo.

Quan l'error apareix de forma dinàmica (després de prémer "Envia", sense recarregar la pàgina), cal a més anunciar-lo encara que el focus estigui en un altre lloc. Per a això existeixen les live regions. Una introducció breu: un contenidor amb role="alert" (o aria-live="assertive") fa que el lector llegeixi automàticament el text que aparegui a dins, sense que l'usuari hagi d'anar a buscar-lo.

<!-- El lector anunciarà el text tan bon punt el teu JS l'insereixi aquí -->
<div role="alert" id="resumen-errores"></div>
// En validar, insereixes el missatge i NVDA el llegeix sol:
document.getElementById('resumen-errores').textContent =
  'Revisa el formulari: hi ha 2 camps amb errors.';

Ens quedem en aquesta introducció: el detall d'aria-live, role="status" vs role="alert" i les regions dinàmiques es veu a nivell de marcatge a 04-03, i la validació completa amb JavaScript (quan validar, com moure el focus, actualitzar missatges en viu) pertany al Mòdul 5.

autocomplete i tipus d'input

Triar bé el type i afegir autocomplete redueix l'esforç de tothom, i de manera molt notable per a la Lucía (menys teclejar), l'Hugo (menys per recordar) i qui utilitza control per veu o mòbil.

Camp type correcte autocomplete Benefici
Correu email email Teclat amb @, validació de format, autoemplenat
Telèfon tel tel Teclat numèric al mòbil
Nom text name Autoemplenat del nom desat
Contrasenya (registre) password new-password El gestor en suggereix una de segura
<!-- CORRECTE: tipus i autocomplete que ajuden el navegador i la persona -->
<label for="email">Correu electrònic</label>
<input type="email" id="email" name="email" autocomplete="email">

<label for="tel">Telèfon</label>
<input type="tel" id="tel" name="tel" autocomplete="tel">

autocomplete compleix a més el criteri 1.3.5 ("Identificar el propòsit de l'entrada"), que ajuda eines que adapten o emplenen camps per l'usuari.

Focus i ordre lògic

L'ordre en què la Lucía arriba als camps en prémer Tab ha de seguir l'ordre visual i lògic del formulari. La clau és que l'ordre del DOM coincideixi amb l'ordre visual: si maquetes de dalt a baix a l'HTML, el focus anirà bé sense trucs.

  • No utilitzis tabindex positiu (tabindex="1", "2"...): trenca l'ordre natural i és una font constant de bugs. Deixa que l'ordre el dicti el DOM.
  • Un <button type="submit"> real permet enviar el formulari amb Enter des de qualsevol camp.
  • Després d'enviar amb errors, portar el focus al resum d'errors o al primer camp fallit estalvia a la Lucía tabular a cegues (implementació amb JS: Mòdul 5).
<!-- INCORRECTE: tabindex positiu força un ordre artificial i fràgil -->
<input id="a" tabindex="3">
<input id="b" tabindex="1">
<input id="c" tabindex="2">
<!-- CORRECTE: sense tabindex; l'ordre del DOM ja és l'ordre de focus -->
<input id="a">
<input id="b">
<input id="c">

Errors Comuns i Consells

  • Camps sense <label> associat. És la fallada número u. Cada control necessita nom per for/id, label envoltant o, en darrer cas, aria-label.
  • Utilitzar el placeholder com a etiqueta. Desapareix, contrasta malament i no és fiable com a nom. Posa sempre etiqueta visible.
  • Obligatorietat només per color. L'asterisc vermell no n'hi ha prou: afegeix required i explica què significa el *.
  • Ajudes i errors sense associar. Un text d'ajuda al costat del camp que no està enllaçat amb aria-describedby és invisible per al lector.
  • Errors només amb vora vermella. La Marta no veu el color: l'error ha d'anar en text i estar associat.
  • Radios/checkboxes sense fieldset/legend. Perden el context de la pregunta.
  • tabindex positiu. Gairebé mai és la solució; ordena el DOM.
  • Consell: emplena el teu formulari sencer només amb el teclat i amb un lector, amb els ulls tancats. Si no saps què demana un camp o on és l'error, encara no és accessible.

Exercicis

Exercici 1: donar nom als camps

Aquest fragment del registre de Cursalia no associa etiquetes. Corregeix-lo utilitzant for/id i substitueix el placeholder-etiqueta per una etiqueta real.

<span>Nom</span>
<input type="text">
<input type="email" placeholder="Correu electrònic">

Exercici 2: agrupar els interessos

Els usuaris poden marcar diversos interessos. Marca correctament aquest grup de checkboxes perquè el lector anunciï el títol del grup.

<p>Interessos</p>
<label><input type="checkbox" name="int" value="dev"> Desenvolupament</label>
<label><input type="checkbox" name="int" value="ux"> Disseny UX</label>
<label><input type="checkbox" name="int" value="data"> Dades</label>

Exercici 3: un camp amb ajuda i error accessibles

Escriu el camp "Contrasenya": etiqueta visible, obligatori, amb una ajuda de format associada i un missatge d'error (associat i no dependent només del color) per a quan la contrasenya sigui massa curta.

Solucions

Solució 1. Cada control rep una etiqueta associada; el placeholder deixa pas a un <label> real:

<label for="nombre">Nom</label>
<input type="text" id="nombre" name="nombre" autocomplete="name">

<label for="email">Correu electrònic</label>
<input type="email" id="email" name="email" autocomplete="email">

Solució 2. El grup sencer va en un <fieldset> amb <legend>:

<fieldset>
  <legend>Interessos</legend>
  <label><input type="checkbox" name="int" value="dev"> Desenvolupament</label>
  <label><input type="checkbox" name="int" value="ux"> Disseny UX</label>
  <label><input type="checkbox" name="int" value="data"> Dades</label>
</fieldset>

Ara el lector anuncia "Interessos, grup" abans de cada casella.

Solució 3. Etiqueta visible + required + ajuda amb aria-describedby + error en text associat, amb icona a més del color:

<label for="pass">Contrasenya *</label>
<input type="password" id="pass" name="pass" required
       autocomplete="new-password"
       aria-describedby="ayuda-pass err-pass"
       aria-invalid="true">

<p id="ayuda-pass">Mínim 8 caràcters, amb una lletra i un número.</p>
<p id="err-pass" class="error">
  <span aria-hidden="true">⚠️</span> Error: la contrasenya és massa curta.
</p>

Fixa't que aria-describedby apunta a dos ids (ajuda i error), així el lector llegeix tots dos. Quan la contrasenya sigui vàlida, el teu codi traurà aria-invalid i el missatge d'error (això ja és lògica de JS, Mòdul 5).

Conclusió

Un formulari accessible no és màgia: és etiquetar cada control, agrupar el que va junt, comunicar l'obligatorietat i les ajudes per text associat, descriure els errors de manera clara i no només per color, i respectar l'ordre de focus del DOM. Amb això, el formulari de matrícula de Cursalia deixa de ser un mur per a la Marta, l'Hugo i la Lucía. Ens hem recolzat gairebé en tot en HTML natiu i només hem tret el cap a aria-describedby, aria-invalid i role="alert".

Aquests atributs aria-* són la porta a la següent lliçó. A 04-03 · Ús de Rols i Propietats ARIA farem un pas enrere per entendre què és ARIA, les seves cinc regles d'or (començant per "no l'utilitzis si hi ha HTML natiu"), com es calcula el nom accessible que hem anat utilitzant (aria-label, aria-labelledby, aria-describedby) i com descriure estats com aria-expanded al desplegable de filtres de Cursalia, sempre a nivell de marcatge.

© Copyright 2026. Tots els drets reservats