Addaw, ir a inicio Donar

Lección 7 de 10 · 15 min de lectura · Revisado el 26/10/2026

Construir y probar un reproductor personalizado accesible

En la lección sobre reproductores y plataformas viste cómo elegir entre el reproductor nativo de HTML5, Able Player, YouTube o Vimeo. Pero muchos proyectos acaban con un reproductor propio: el diseño quiere controles con la identidad de la marca, o la plataforma de formación necesita funciones que los demás no tienen. Es uno de los componentes más difíciles de hacer bien, porque combina botones con estados, deslizadores, atajos de teclado, pantalla completa y pistas de texto. Esta lección muestra cómo construirlo sobre la API de medios de HTML y cómo probarlo antes de publicarlo.

Antes de escribir código

La primera pregunta es si hace falta. Un reproductor existente y probado (Able Player, o el nativo con sus controles) suele ser más accesible que uno hecho desde cero con prisas. Si se decide construirlo, conviene escribir antes los requisitos que debe cumplir, porque son los que se auditarán:

  • 2.1.1 Teclado y 2.1.2 Sin trampas para el foco: todos los controles operables con teclado, y se puede salir del reproductor con Tab.
  • 4.1.2 Nombre, función, valor: cada control con nombre, función y estado; los deslizadores, con un valor comprensible.
  • 2.5.3 Etiqueta en el nombre: si el botón muestra un texto o una descripción emergente, el nombre accesible lo contiene.
  • 2.4.7 Foco visible y 2.4.11 Foco no oculto (mínimo): el foco se ve y no queda tapado por los controles que se esconden.
  • 2.5.8 Tamaño del objetivo (mínimo): al menos 24 × 24 píxeles CSS (mejor 44 × 44) y 1.4.11 Contraste no textual de los iconos sobre el vídeo.
  • 2.1.4 Atajos de teclado si hay atajos de una sola tecla, y 1.4.13 Contenido con hover o foco para las descripciones emergentes.
  • EN 301 549 (PDF, se abre en una ventana nueva) 7.1.1, 7.1.4, 7.2.1 y 7.3: mostrar subtítulos, personalizarlos, seleccionar la audiodescripción y tener esos controles al mismo nivel que los principales.

Marcado: elementos nativos con nombre y estado

El error de base es construir los controles con <div> y <span> que solo responden al ratón:

<!-- Incorrecto: controles que no existen para el teclado ni para el 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ón correcta usa <button> e <input type="range">, que ya son operables con teclado y tienen función conocida por la tecnología de apoyo:

<!-- Correcto: botones y deslizadores nativos, con nombre y estado -->
<div class="reproductor" role="group" aria-label="Reproductor: Jornada de puertas abiertas">
  <video id="v" preload="metadata" playsinline>
    <source src="jornada.mp4" type="video/mp4">
    <track kind="captions" src="jornada-es.vtt" srclang="es" label="Español">
  </video>
  <div class="controles">
    <button type="button" id="play" aria-label="Reproducir">
      <svg aria-hidden="true" focusable="false">...</svg>
    </button>
    <label for="pos" class="solo-lectores">Posición</label>
    <input type="range" id="pos" min="0" max="100" step="1" value="0"
           aria-valuetext="0 segundos de 2 minutos 10 segundos">
    <button type="button" id="mute" aria-pressed="false">Silenciar</button>
    <button type="button" id="cc" aria-pressed="false">Subtítulos</button>
    <button type="button" id="ad" aria-pressed="false">Audiodescripción</button>
    <button type="button" id="vel">Velocidad: 1x</button>
    <button type="button" id="fs">Pantalla completa</button>
  </div>
</div>

Dos patrones para los botones

Hay dos formas correctas de comunicar el estado, y no deben mezclarse. En reproducir/pausar lo habitual es cambiar el nombre («Reproducir» pasa a «Pausar») y no usar aria-pressed. En subtítulos, silencio y audiodescripción lo claro es un nombre fijo con estado: «Subtítulos, botón de alternancia, pulsado». Si se cambia el nombre y además el estado («Desactivar subtítulos, pulsado»), el anuncio resulta contradictorio. Los iconos SVG llevan aria-hidden="true" y el nombre va en el botón.

Deslizadores con valor comprensible

Un deslizador de posición que anuncia «37» no dice nada. El atributo aria-valuetext traduce el valor a lenguaje humano: «1 minuto 20 segundos de 2 minutos 10 segundos». Actualízalo con moderación (por ejemplo, una vez por segundo o al soltar), porque algunos lectores de pantalla anuncian cada cambio del control que tiene el foco. Las flechas deben avanzar un paso útil, como 5 segundos, y el volumen debe ser un deslizador propio, independiente del botón de silencio.

Comportamiento con JavaScript

La API HTMLMediaElement (play(), pause(), currentTime, playbackRate) y la API de pistas de texto (textTracks y su propiedad mode) bastan para lo esencial:

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';

// Reproducir / pausar: cambia el nombre, no se usa aria-pressed
play.addEventListener('click', () => { v.paused ? v.play() : v.pause(); });
v.addEventListener('play',  () => play.setAttribute('aria-label', 'Pausar'));
v.addEventListener('pause', () => play.setAttribute('aria-label', 'Reproducir'));

// Subtítulos: nombre fijo y estado pulsado / no pulsado
cc.addEventListener('click', () => {
  const activar = cc.getAttribute('aria-pressed') !== 'true';
  pista.mode = activar ? 'showing' : 'hidden';
  cc.setAttribute('aria-pressed', String(activar));
});

// Audiodescripción como versión alternativa: se conserva el minuto
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 contenedor, no sobre el vídeo
document.getElementById('fs').addEventListener('click', () => {
  document.querySelector('.reproductor').requestFullscreen();
});

Fíjate en tres detalles. La audiodescripción se ofrece cambiando a la versión audiodescrita y conservando el minuto, porque la API de pistas de audio alternativas tiene un soporte muy desigual en los navegadores. La velocidad (playbackRate) es una necesidad que recoge la nota MAUR y que los navegadores aplican sin alterar el tono de la voz. Y la pantalla completa se pide sobre el contenedor: si se pide sobre el <video>, los controles propios y los subtítulos dibujados por el reproductor desaparecen en pantalla completa.

Atajos de teclado sin conflictos

Los atajos de una sola tecla (K para pausar, C para subtítulos) ayudan, pero el criterio 2.1.4 exige que se puedan desactivar, reasignar o que solo funcionen cuando el foco está en el componente. Lo más sencillo es lo último:

// Atajos de una tecla solo cuando el foco está dentro del reproductor (2.1.4)
const rep = document.querySelector('.reproductor');
rep.addEventListener('keydown', (e) => {
  if (e.ctrlKey || e.altKey || e.metaKey) return; // no interferir con atajos del navegador
  if (e.key === 'k') { v.paused ? v.play() : v.pause(); }
  if (e.key === 'c') { cc.click(); }
});

Error frecuente

Añadir un manejador global de la barra espaciadora para «reproducir y pausar». Cuando el foco está en un botón, la barra espaciadora ya lo activa: el resultado es que el vídeo se pausa y se reanuda a la vez, o que la barra deja de servir para pulsar «Subtítulos». Además, una persona que escribe en un campo de comentarios bajo el vídeo lo pausa con cada espacio. No interceptes teclas que el navegador ya gestiona.

CSS: controles visibles, contrastados y personalizables

/* Los controles no se ocultan mientras haya foco dentro */
.reproductor:focus-within .controles,
.reproductor:hover .controles { opacity: 1; }

/* Fondo semiopaco para el contraste de iconos 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; } /* respeta el modo de colores forzados */

/* Subtítulos personalizables por la persona */
.reproductor[data-subs="grande"] video::cue { font-size: 150%; }
.reproductor[data-subs="fondo"] video::cue { background: #000; color: #ff0; }

Los controles que se ocultan tras unos segundos de inactividad deben reaparecer al recibir el foco y no desaparecer mientras el foco esté dentro (2.4.11). El fondo semiopaco garantiza el contraste de los iconos sobre cualquier fotograma. Con fill: currentColor los iconos siguen el color del texto en el modo de colores forzados de Windows. Y el pseudoelemento ::cue permite ofrecer un pequeño panel de preferencias de subtítulos (tamaño, color, fondo), que conviene recordar entre visitas, como pide el requisito 7.1.4 de la EN 301 549.

Punto clave

Un reproductor propio es un componente con estado, como un diálogo o unas pestañas. Trátalo igual: patrón documentado, elementos nativos, nombres y estados verificados con lector de pantalla y una batería de pruebas que se repite en cada versión. Una regresión en el botón de subtítulos deja sin acceso a todo el catálogo de vídeos de golpe.

Protocolo de pruebas

PruebaCómoResultado esperado
TecladoRecorrer con Tab y Mayús+Tab; activar con Intro y espacio; flechas en los deslizadoresTodos los controles alcanzables, foco visible, se puede salir
Lector de pantallaNVDA con Chrome o Firefox; VoiceOver en macOS e iOS; TalkBack en AndroidNombres, funciones y estados correctos; posición anunciada en minutos y segundos
Subtítulos y audiodescripciónActivar, desactivar, cambiar de minuto y pasar a pantalla completaSiguen sincronizados y visibles; la audiodescripción conserva el minuto
Zoom y reflujoZoom al 200 % y ventana de 320 píxeles de anchoLos controles no se solapan ni se salen de la pantalla
Colores forzadosModo de contraste de WindowsIconos y foco visibles
Control por vozDecir «pulsar Subtítulos» con Control por voz o Acceso por vozEl nombre visible activa el control

Para las pruebas con lector de pantalla y teclado puedes seguir el protocolo del curso «Revisión manual con tecnologías de apoyo» del nivel 4. En iOS, recuerda que el atributo playsinline evita que Safari pase al reproductor nativo a pantalla completa al pulsar reproducir, lo que haría invisibles tus controles.

Pruébalo tú

Abre una página con un reproductor propio (el de un medio de comunicación o una plataforma de cursos). Recorre sus controles con Tab y anota cuáles no reciben el foco. Después abre las herramientas para desarrolladores de Chrome, pestaña «Elementos», panel «Accesibilidad», y comprueba el nombre y la función de los botones de subtítulos y de reproducción. ¿Alguno se llama solo «botón» o carece de estado?

Resumen

  • Antes de construir un reproductor propio, valora uno existente; si se construye, sus requisitos (2.1.1, 2.1.4, 2.4.11, 2.5.8, 4.1.2 y capítulo 7 de la EN 301 549) se escriben antes que el código.
  • Usa <button> e <input type="range">; reproducir/pausar cambia el nombre, y subtítulos, silencio y audiodescripción usan nombre fijo con aria-pressed.
  • Los deslizadores necesitan aria-valuetext en minutos y segundos; la audiodescripción conserva el minuto y la pantalla completa se pide sobre el contenedor.
  • Los atajos de una tecla solo con el foco dentro del reproductor; nunca interceptes la barra espaciadora global.
  • Prueba en cada versión con teclado, lector de pantalla, zoom, colores forzados y control por voz.

Comprueba lo que has aprendido

Esta lección se puede leer libremente. Para hacer su test de 3 preguntas, los ejercicios prácticos con páginas reales, guardar tu avance y optar al examen y al certificado, matricúlate en el curso.

Matricularme · nivel 6 (119 €)