Lliçó 7 de 10 · 15 min de lectura · Revisada el 26/10/2026
Construir i provar un reproductor personalitzat accessible
Estàs llegint una lliçó oberta. Matricula't per fer els tests i els exercicis, desar el teu progrés i aconseguir el certificat.
Matricula-m'hi · nivell 6 (119 €)A la lliçó sobre reproductors i plataformes vas veure com triar entre el reproductor natiu d'HTML5, Able Player, YouTube o Vimeo. Però molts projectes acaben amb un reproductor propi: el disseny vol controls amb la identitat de la marca, o la plataforma de formació necessita funcions que els altres no tenen. És un dels components més difícils de fer bé, perquè combina botons amb estats, controls lliscants, dreceres de teclat, pantalla completa i pistes de text. Aquesta lliçó mostra com construir-lo sobre l'API de mitjans d'HTML i com provar-lo abans de publicar-lo.
Abans d'escriure codi
La primera pregunta és si cal. Un reproductor existent i provat (Able Player, o el natiu amb els seus controls) sol ser més accessible que un de fet des de zero amb presses. Si es decideix construir-lo, convé escriure abans els requisits que ha de complir, perquè són els que s'auditaran:
- 2.1.1 Teclat i 2.1.2 Sense paranys per al focus del teclat: tots els controls operables amb el teclat, i es pot sortir del reproductor amb Tab.
- 4.1.2 Nom, funció i valor: cada control amb nom, funció i estat; els controls lliscants, amb un valor comprensible.
- 2.5.3 Etiqueta en el nom: si el botó mostra un text o una descripció emergent, el nom accessible el conté.
- 2.4.7 Focus visible i 2.4.11 Focus no ocult (mínim): el focus es veu i no queda tapat pels controls que s'amaguen.
- 2.5.8 Mida de l'objectiu (mínim): com a mínim 24 × 24 píxels CSS (millor 44 × 44) i 1.4.11 Contrast no textual de les icones sobre el vídeo.
- 2.1.4 Dreceres de teclat si hi ha dreceres d'una sola tecla, i 1.4.13 Contingut en passar el cursor o en rebre el focus per a les descripcions emergents.
- EN 301 549 (PDF, s'obre en una finestra nova) 7.1.1, 7.1.4, 7.2.1 i 7.3: mostrar els subtítols, personalitzar-los, seleccionar l'audiodescripció i tenir aquests controls al mateix nivell que els principals.
Marcatge: elements natius amb nom i estat
L'error de base és construir els controls amb <div> i <span> que només responen al ratolí:
<!-- Incorrecte: controls que no existeixen per al teclat ni per al lector de pantalla -->
<div class="controles">
<div class="icono-play" onclick="alternar()"><svg>...</svg></div>
<div class="barra" onclick="buscar(event)"><div class="relleno"></div></div>
<span class="cc" onclick="subtitulos()">CC</span>
</div>La versió correcta fa servir <button> i <input type="range">, que ja són operables amb el teclat i tenen una funció coneguda per la tecnologia de suport:
<!-- Correcte: botons i controls lliscants natius, amb nom i estat -->
<div class="reproductor" role="group" aria-label="Reproductor: Jornada de portes obertes">
<video id="v" preload="metadata" playsinline>
<source src="jornada.mp4" type="video/mp4">
<track kind="captions" src="jornada-es.vtt" srclang="es" label="Espanyol">
</video>
<div class="controles">
<button type="button" id="play" aria-label="Reprodueix">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
<label for="pos" class="solo-lectores">Posició</label>
<input type="range" id="pos" min="0" max="100" step="1" value="0"
aria-valuetext="0 segons de 2 minuts 10 segons">
<button type="button" id="mute" aria-pressed="false">Silencia</button>
<button type="button" id="cc" aria-pressed="false">Subtítols</button>
<button type="button" id="ad" aria-pressed="false">Audiodescripció</button>
<button type="button" id="vel">Velocitat: 1x</button>
<button type="button" id="fs">Pantalla completa</button>
</div>
</div>Dos patrons per als botons
Hi ha dues maneres correctes de comunicar l'estat, i no s'han de barrejar. En reproduir/posar en pausa el més habitual és canviar el nom («Reprodueix» passa a «Posa en pausa») i no fer servir aria-pressed. En subtítols, silenci i audiodescripció el més clar és un nom fix amb estat: «Subtítols, botó de commutació, premut». Si es canvia el nom i, a més, l'estat («Desactiva els subtítols, premut»), l'anunci resulta contradictori. Les icones SVG porten aria-hidden="true" i el nom va al botó.
Controls lliscants amb un valor comprensible
Un control lliscant de posició que anuncia «37» no diu res. L'atribut aria-valuetext tradueix el valor a llenguatge humà: «1 minut 20 segons de 2 minuts 10 segons». Actualitza'l amb moderació (per exemple, un cop per segon o en deixar-lo anar), perquè alguns lectors de pantalla anuncien cada canvi del control que té el focus. Les fletxes han d'avançar un pas útil, com ara 5 segons, i el volum ha de ser un control lliscant propi, independent del botó de silenci.
Comportament amb JavaScript
L'API HTMLMediaElement (play(), pause(), currentTime, playbackRate) i l'API de pistes de text (textTracks i la seva propietat mode) són suficients per a l'essencial:
const v = document.getElementById('v');
const play = document.getElementById('play');
const cc = document.getElementById('cc');
const ad = document.getElementById('ad');
const pista = v.textTracks[0];
pista.mode = 'hidden';
// Reproduir / posar en pausa: canvia el nom, no es fa servir aria-pressed
play.addEventListener('click', () => { v.paused ? v.play() : v.pause(); });
v.addEventListener('play', () => play.setAttribute('aria-label', 'Posa en pausa'));
v.addEventListener('pause', () => play.setAttribute('aria-label', 'Reprodueix'));
// Subtítols: nom fix i estat premut / no premut
cc.addEventListener('click', () => {
const activar = cc.getAttribute('aria-pressed') !== 'true';
pista.mode = activar ? 'showing' : 'hidden';
cc.setAttribute('aria-pressed', String(activar));
});
// Audiodescripció com a versió alternativa: es conserva el minut
ad.addEventListener('click', () => {
const activar = ad.getAttribute('aria-pressed') !== 'true';
const t = v.currentTime, enMarcha = !v.paused;
v.src = activar ? 'jornada-ad.mp4' : 'jornada.mp4';
v.addEventListener('loadedmetadata', () => {
v.currentTime = t;
if (enMarcha) v.play();
}, { once: true });
ad.setAttribute('aria-pressed', String(activar));
});
// Pantalla completa sobre el contenidor, no sobre el vídeo
document.getElementById('fs').addEventListener('click', () => {
document.querySelector('.reproductor').requestFullscreen();
});Fixa't en tres detalls. L'audiodescripció s'ofereix canviant a la versió audiodescrita i conservant el minut, perquè l'API de pistes d'àudio alternatives té un suport molt desigual als navegadors. La velocitat (playbackRate) és una necessitat que recull la nota MAUR i que els navegadors apliquen sense alterar el to de la veu. I la pantalla completa es demana sobre el contenidor: si es demana sobre el <video>, els controls propis i els subtítols dibuixats pel reproductor desapareixen en pantalla completa.
Dreceres de teclat sense conflictes
Les dreceres d'una sola tecla (K per posar en pausa, C per als subtítols) ajuden, però el criteri 2.1.4 exigeix que es puguin desactivar, reassignar o que només funcionin quan el focus és al component. El més senzill és l'últim:
// Dreceres d'una tecla només quan el focus és dins del reproductor (2.1.4)
const rep = document.querySelector('.reproductor');
rep.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.altKey || e.metaKey) return; // no interferir amb dreceres del navegador
if (e.key === 'k') { v.paused ? v.play() : v.pause(); }
if (e.key === 'c') { cc.click(); }
});Error freqüent
Afegir un gestor global de la barra espaiadora per «reproduir i posar en pausa». Quan el focus és en un botó, la barra espaiadora ja l'activa: el resultat és que el vídeo es posa en pausa i es reprèn alhora, o que la barra deixa de servir per prémer «Subtítols». A més, una persona que escriu en un camp de comentaris sota el vídeo el posa en pausa amb cada espai. No interceptis tecles que el navegador ja gestiona.
CSS: controls visibles, contrastats i personalitzables
/* Els controls no s'amaguen mentre hi hagi focus a dins */
.reproductor:focus-within .controles,
.reproductor:hover .controles { opacity: 1; }
/* Fons semiopac per al contrast de les icones sobre el vídeo (1.4.11) */
.controles { background: rgb(0 0 0 / 0.75); }
.controles button { min-width: 44px; min-height: 44px; color: #fff; }
.controles svg { fill: currentColor; } /* respecta el mode de colors forçats */
/* Subtítols personalitzables per la persona */
.reproductor[data-subs="grande"] video::cue { font-size: 150%; }
.reproductor[data-subs="fondo"] video::cue { background: #000; color: #ff0; }Els controls que s'amaguen després d'uns segons d'inactivitat han de reaparèixer en rebre el focus i no desaparèixer mentre el focus sigui a dins (2.4.11). El fons semiopac garanteix el contrast de les icones sobre qualsevol fotograma. Amb fill: currentColor les icones segueixen el color del text en el mode de colors forçats de Windows. I el pseudoelement ::cue permet oferir un petit tauler de preferències de subtítols (mida, color, fons), que convé recordar entre visites, com demana el requisit 7.1.4 de l'EN 301 549.
Punt clau
Un reproductor propi és un component amb estat, com un diàleg o unes pestanyes. Tracta'l igual: patró documentat, elements natius, noms i estats verificats amb lector de pantalla i una bateria de proves que es repeteix a cada versió. Una regressió al botó de subtítols deixa sense accés tot el catàleg de vídeos de cop.
Protocol de proves
| Prova | Com | Resultat esperat |
|---|---|---|
| Teclat | Recórrer amb Tab i Maj+Tab; activar amb Retorn i Espai; fletxes als controls lliscants | Tots els controls abastables, focus visible, es pot sortir |
| Lector de pantalla | NVDA amb Chrome o Firefox; VoiceOver a macOS i iOS; TalkBack a Android | Noms, funcions i estats correctes; posició anunciada en minuts i segons |
| Subtítols i audiodescripció | Activar, desactivar, canviar de minut i passar a pantalla completa | Continuen sincronitzats i visibles; l'audiodescripció conserva el minut |
| Zoom i reajustament | Zoom al 200 % i finestra de 320 píxels d'amplada | Els controls no se superposen ni surten de la pantalla |
| Colors forçats | Mode de contrast de Windows | Icones i focus visibles |
| Control per veu | Dir «tocar Subtítols» amb Control per veu o Accés per veu | El nom visible activa el control |
Per a les proves amb lector de pantalla i teclat pots seguir el protocol del curs «Revisió manual amb tecnologies de suport» del nivell 4. A iOS, recorda que l'atribut playsinline evita que Safari passi al reproductor natiu a pantalla completa en prémer reproduir, cosa que faria invisibles els teus controls.
Prova-ho tu
Obre una pàgina amb un reproductor propi (el d'un mitjà de comunicació o d'una plataforma de cursos). Recorre'n els controls amb Tab i anota quins no reben el focus. Després obre les eines per a desenvolupadors de Chrome, pestanya «Elements», tauler «Accessibilitat», i comprova el nom i la funció dels botons de subtítols i de reproducció. N'hi ha algun que es digui només «botó» o que no tingui estat?
Resum
- Abans de construir un reproductor propi, valora'n un d'existent; si es construeix, els seus requisits (2.1.1, 2.1.4, 2.4.11, 2.5.8, 4.1.2 i capítol 7 de l'EN 301 549) s'escriuen abans que el codi.
- Fes servir
<button>i<input type="range">; reproduir/posar en pausa canvia el nom, i subtítols, silenci i audiodescripció fan servir nom fix ambaria-pressed. - Els controls lliscants necessiten
aria-valuetexten minuts i segons; l'audiodescripció conserva el minut i la pantalla completa es demana sobre el contenidor. - Les dreceres d'una tecla, només amb el focus dins del reproductor; no interceptis mai la barra espaiadora global.
- Prova a cada versió amb teclat, lector de pantalla, zoom, colors forçats i control per veu.
Portes 6 lliçons d'aquest curs: que no es perdin
Crea el teu compte i desa el que ja has llegit i segueix el curs amb tot:
- El test d'aquesta lliçó (3 preguntes) per comprovar el que has après
- Exercicis pràctics amb pàgines reals
- El teu progrés desat: continues on ho vas deixar, des de qualsevol dispositiu
- Constància del curs amb les hores i certificat del nivell 6 quan aprovis l'examen
Entres amb el teu compte de Google o només amb el teu correu electrònic. No cal contrasenya.
Matricula-m'hi · nivell 6 (119 €)