¿Qué aprenderás?
-
Manejar elementos <audio> (métodos .play(), .pause(), .currentTime = 0).
-
Usar event delegation para no crear un listener por botón.
-
Micro-interacciones: foco de teclado, estados activos, y layout responsivo.
Código (3 archivos)
1) index.html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sound Board</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="container">
<h1>Sound Board</h1>
<p class="lead">Haz clic o usa <kbd>Tab</kbd> + <kbd>Enter</kbd>.</p>
<section class="grid" id="soundGrid" aria-label="Tablero de sonidos">
<!-- Cada botón declara data-sound que apunta al id del <audio> -->
<button class="pad" data-sound="s1">Roar</button>
<button class="pad" data-sound="s2">Clang</button>
<button class="pad" data-sound="s3">Pop</button>
<button class="pad" data-sound="s4">Bell</button>
</section>
<!-- Audios (puedes cambiar las URLs por tus archivos .mp3/.ogg) -->
<audio id="s1" src="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3" preload="auto"></audio>
<audio id="s2" src="https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg" preload="auto"></audio>
<audio id="s3" src="https://actions.google.com/sounds/v1/cartoon/pop.ogg" preload="auto"></audio>
<audio id="s4" src="https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg" preload="auto"></audio>
</main>
<script src="script.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sound Board</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="container">
<h1>Sound Board</h1>
<p class="lead">Haz clic o usa <kbd>Tab</kbd> + <kbd>Enter</kbd>.</p>
<section class="grid" id="soundGrid" aria-label="Tablero de sonidos">
<!-- Cada botón declara data-sound que apunta al id del <audio> -->
<button class="pad" data-sound="s1">Roar</button>
<button class="pad" data-sound="s2">Clang</button>
<button class="pad" data-sound="s3">Pop</button>
<button class="pad" data-sound="s4">Bell</button>
</section>
<!-- Audios (puedes cambiar las URLs por tus archivos .mp3/.ogg) -->
<audio id="s1" src="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3" preload="auto"></audio>
<audio id="s2" src="https://actions.google.com/sounds/v1/cartoon/clang_and_wobble.ogg" preload="auto"></audio>
<audio id="s3" src="https://actions.google.com/sounds/v1/cartoon/pop.ogg" preload="auto"></audio>
<audio id="s4" src="https://actions.google.com/sounds/v1/alarms/alarm_clock.ogg" preload="auto"></audio>
</main>
<script src="script.js"></script>
</body>
</html>
2) style.css
* { box-sizing: border-box; }
:root {
--bg: #0f1115; --card: #151a23; --txt: #f5f7fa; --muted: #9aa4b2;
--accent: #7c5cff; --ring: #8b5cf6; --radius: 18px; --gap: 14px;
}
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: radial-gradient(1200px 600px at 20% -10%, #1b2030 0, #0f1115 45%, #0b0e13 100%);
color: var(--txt);
}
.container {
max-width: 1000px; margin: 48px auto; padding: 0 16px;
}
h1 { margin: 0 0 4px; font-size: clamp(28px, 4vw, 44px); }
.lead { margin: 0 0 24px; color: var(--muted); }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: var(--gap);
}
.pad {
border: 0; outline: none; cursor: pointer;
padding: 28px 20px; border-radius: var(--radius);
background: linear-gradient(180deg, #1b2130, var(--card));
color: #fff; font-weight: 700; letter-spacing: .4px;
box-shadow: 0 1px 0 rgba(255,255,255,.04) inset, 0 12px 30px rgba(0,0,0,.35);
transition: transform .12s ease, box-shadow .18s ease, background .18s ease;
}
.pad:hover { transform: translateY(-2px); }
.pad:active, .pad.is-playing {
transform: translateY(0);
box-shadow: 0 0 0 2px #000, 0 0 0 4px var(--ring), 0 12px 34px rgba(0,0,0,.5);
background: linear-gradient(180deg, #21283a, #18202f);
}
.pad:focus-visible {
outline: 0;
box-shadow: 0 0 0 2px #000, 0 0 0 4px var(--accent);
transition: box-shadow .08s ease;
}
* { box-sizing: border-box; }
:root {
--bg: #0f1115; --card: #151a23; --txt: #f5f7fa; --muted: #9aa4b2;
--accent: #7c5cff; --ring: #8b5cf6; --radius: 18px; --gap: 14px;
}
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
background: radial-gradient(1200px 600px at 20% -10%, #1b2030 0, #0f1115 45%, #0b0e13 100%);
color: var(--txt);
}
.container {
max-width: 1000px; margin: 48px auto; padding: 0 16px;
}
h1 { margin: 0 0 4px; font-size: clamp(28px, 4vw, 44px); }
.lead { margin: 0 0 24px; color: var(--muted); }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: var(--gap);
}
.pad {
border: 0; outline: none; cursor: pointer;
padding: 28px 20px; border-radius: var(--radius);
background: linear-gradient(180deg, #1b2130, var(--card));
color: #fff; font-weight: 700; letter-spacing: .4px;
box-shadow: 0 1px 0 rgba(255,255,255,.04) inset, 0 12px 30px rgba(0,0,0,.35);
transition: transform .12s ease, box-shadow .18s ease, background .18s ease;
}
.pad:hover { transform: translateY(-2px); }
.pad:active, .pad.is-playing {
transform: translateY(0);
box-shadow: 0 0 0 2px #000, 0 0 0 4px var(--ring), 0 12px 34px rgba(0,0,0,.5);
background: linear-gradient(180deg, #21283a, #18202f);
}
.pad:focus-visible {
outline: 0;
box-shadow: 0 0 0 2px #000, 0 0 0 4px var(--accent);
transition: box-shadow .08s ease;
}
3) script.js
// Event delegation: un solo listener para todos los botones
const grid = document.getElementById('soundGrid');
function playSound(audio) {
if (!audio) return;
// Reinicia y reproduce (si otra suena, no pasa nada: los audios son independientes)
audio.pause();
audio.currentTime = 0;
audio.play().catch(() => {
// Algunos navegadores bloquean autoplay hasta que hay interacción real
// Aquí ya hubo clic/tecla, así que en general no habrá error.
});
}
grid.addEventListener('click', (e) => {
const btn = e.target.closest('.pad');
if (!btn) return;
const id = btn.dataset.sound;
const audio = document.getElementById(id);
// Feedback visual mientras suena
btn.classList.add('is-playing');
playSound(audio);
const removeState = () => btn.classList.remove('is-playing');
// Quita estado al terminar (si el formato emite 'ended')
audio?.addEventListener('ended', removeState, { once: true });
// Fallback: quítalo tras N ms si no hay 'ended' (p. ej., loop o stream)
setTimeout(removeState, 1500);
});
// Soporte teclado: Enter/Space activan el botón enfocado
grid.addEventListener('keydown', (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
const btn = e.target.closest('.pad');
if (!btn) return;
e.preventDefault();
btn.click();
});
// Event delegation: un solo listener para todos los botones
const grid = document.getElementById('soundGrid');
function playSound(audio) {
if (!audio) return;
// Reinicia y reproduce (si otra suena, no pasa nada: los audios son independientes)
audio.pause();
audio.currentTime = 0;
audio.play().catch(() => {
// Algunos navegadores bloquean autoplay hasta que hay interacción real
// Aquí ya hubo clic/tecla, así que en general no habrá error.
});
}
grid.addEventListener('click', (e) => {
const btn = e.target.closest('.pad');
if (!btn) return;
const id = btn.dataset.sound;
const audio = document.getElementById(id);
// Feedback visual mientras suena
btn.classList.add('is-playing');
playSound(audio);
const removeState = () => btn.classList.remove('is-playing');
// Quita estado al terminar (si el formato emite 'ended')
audio?.addEventListener('ended', removeState, { once: true });
// Fallback: quítalo tras N ms si no hay 'ended' (p. ej., loop o stream)
setTimeout(removeState, 1500);
});
// Soporte teclado: Enter/Space activan el botón enfocado
grid.addEventListener('keydown', (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return;
const btn = e.target.closest('.pad');
if (!btn) return;
e.preventDefault();
btn.click();
});
Arquitectura (capa por capa)
1) Estructura HTML
-
Un contenedor .grid con botones <button> (no <div>), porque:
-
Son focusables por defecto (teclado).
-
Emite eventos de “click” al presionar Enter/Space.
-
-
Cada botón tiene un atributo data-sound="ID". Ese ID coincide con el id de un <audio> en el HTML.
Ej.: <button data-sound="s1">Roar</button> ↔ <audio id="s1" src="...">.Imagen ilustrativa · Compagnons / Unsplash.
Esto desacopla el UI del recurso; no necesitas hardcodear rutas de audio dentro del botón.
2) Estilos y layout
-
CSS Grid para que la cuadrícula responda al ancho de pantalla (repeat(auto-fit, minmax(...))).
-
Estados visuales:
-
:hover, :active, y una clase .is-playing que añadimos por JS mientras suena el audio.
-
Sombras y un leve “presionado” para que el usuario sienta el clic.
-
3) Lógica JS
-
Event delegation: en vez de asignar un addEventListener por botón, ponemos uno en el contenedor .grid y filtramos con e.target.closest('.pad').
Ventajas: menos listeners, más simple escalar a 50+ botones. -
Reproducción:
-
audio.pause() y audio.currentTime = 0 reinician la pista para evitar que se “encimen” clics rápidos.
-
audio.play() inicia. Si el navegador exige interacción previa, ya la hubo (clic/tecla), por lo que no debería bloquear.
-
-
Feedback:
-
Añadimos .is-playing al botón y la retiramos cuando el audio lanza ended.
-
Fallback con setTimeout por si el audio no emite ended (p. ej., si cambias a loop).
-
Detalles importantes y pitfalls
Autoplay / políticas de los navegadores
-
La mayoría de navegadores bloquean play() hasta que haya interacción del usuario (clic, tecla).
-
En este proyecto disparas play() desde el handler del clic/tecla: cumple la regla; todo bien.
-
Si quisieras pre-cargar o reproducir “al cargar la página”, te toparás con bloqueos; usa preload="auto" y reproduce solo tras el primer input.
Ejemplo )
Finalmente debe de quedar algo como el siguiente ejemplo