Drum Kit with HTML, CSS & JavaScript
20 DAYS 20 PROJECT CHALLENGE
Day #11
Project Overview
The Drum Kit is a simple and interactive project built using HTML, CSS, and JavaScript. It allows the user to play drum sounds by clicking on the drums or pressing corresponding keys on the keyboard. Such sounds as Kick, Snare, Hi-Hat, Tom, Crash, Ride, Clap, and Percussion are available in the application.
The project uses Web Audio API to create sounds and provide multiple fallbacks in case a browser does not support a particular sample. In addition, the user gets visual feedback when they press a drum pad to make the application more engaging. Thus, the Drum Kit project is beneficial for the beginner as it covers such topics as keyboard events, mouse interaction, dynamic content in DOM, playing sounds, and interaction with JavaScript.
Key Features
- Eight Different Drum Sounds: The Drum Kit offers 8 types of sounds, including Kick, Snare, Hi-Hat, Tom, Crash, Ride, Clap, and Percussion. These sounds are assigned to specific drum pads.
- Mouse Click Controls: Users can play a drum sound by clicking directly on a drum pad. This makes the application simple and easy to use, especially for users who prefer mouse or touch interactions.
- Keyboard Controls: All drum sounds are also linked with keys from A to K. The letters on the keyboard correspond to particular drum pads, and one can use them to trigger the desired sounds.
- Dynamic Drum Pads: JavaScript dynamically creates drum pads, which helps organize and optimize the code. Developers can also update the PADS array, which contains all drum pad properties and settings.
- Web Audio API: This project uses the Web Audio API to load and play audio samples. This allows the application to control sound playback and volume more effectively.
- Audio Sample Playback: The application tries to fetch the audio file from a particular folder (sounds), and, if it is successful, it plays the desired sound.
- Fallback Audio System: If the audio buffer is not defined, the application attempts to use HTMLAudioElement. If it is not available, the script will create a simple drum sound with JavaScript.
- Lazy Audio Initialization: The application does not load audio files when it is opened but initializes them when the user interacts with it for the first time. It optimizes the performance of the application and prevents it from loading too many files at once.
- Responsive Grid: The drum pads are displayed in a CSS Grid layout. On smaller screens, the number of columns changes so the pads remain easy to view and interact with.
- Interactive Experience: By combining mouse interaction, keyboard interaction, sound effects, and animation, the application provides an engaging interactive experience similar to a real drum kit.
What You'll Learn
- Understand how to handle keyboard events using JavaScript.
- Learn how to respond to mouse and pointer events.
- Practice creating HTML elements dynamically with JavaScript.
- Understand how arrays and objects can store application data.
- Know how to use the Web Audio API.
- Practice loading audio files using
fetch(). - Understand how
AudioBuffercan be used for sound playback. - Learn how fallback systems can handle missing resources.
- Practice using CSS classes to create interactive animations.
- Understand how JavaScript can change sound volume dynamically.
- Learn how
Mapcan store related application data.
HTML Code
HTML creates the basic structure of the Drum Kit. The main application is placed inside the .card container, which contains the project heading and the drum kit area. The #kit section acts as the main container for all drum pads. Instead of writing every pad manually in the HTML, JavaScript creates the drum pads dynamically based on the available drum sound data. Each generated pad displays the drum sound name, its keyboard key, and instructions explaining that the sound can be played by clicking or pressing the corresponding key.
The legend muted inside the drum kit displays the available keyboard controls, including A, S, D, F, G, H, J, and K. Also, the <details> section provides a short explanation of how the sound system works and in last script.js is linked at the bottom of the <body> so JavaScript can create the drum pads, handle user interactions, and manage audio playback.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Day 11 — Drum Kit</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="card" role="main" aria-labelledby="title">
<header>
<div class="logo">DK</div>
<div>
<h1 id="title">Day 11: Drum Kit</h1>
<p class="lead">Play drum sounds by clicking pads or pressing keys. Uses audio samples with a Web Audio
fallback.</p>
</div>
</header>
<section class="kit" id="kit" aria-label="Drum kit"> <!-- pads generated by JS -->
<!-- Example keyboard mapping shown below for reference -->
<div class="legend muted">Keys: A S D F G H J K</div>
</section>
<details style="margin-top:12px">
<summary>How it works (short)</summary>
<p class="small">JS loads audio samples into Audio elements or AudioBuffer via Web Audio. When a pad is
triggered (click or keydown) the corresponding sound is played and the pad gets an active visual state.
If sample files are missing, a small synthesized sound is generated as a fallback.</p>
</details>
</main>
<script src="script.js"></script>
</body>
</html> CSS Code
The CSS code controls the appearance and layout of the Drum Kit. The :root section defines reusable values for the background, card colors, accent color, muted text, and other design elements. While the .card class styles the main application container by adding padding, rounded corners, a border, and a shadow and .kit class uses CSS Grid to arrange the drum pads into four columns. A media query changes the layout to two columns on smaller screens.
Each drum pad uses the .pad class, which controls its size, spacing, border, cursor, and layout. Flexbox centers the content inside each pad. The .label, .key, and .sub classes style the drum name, keyboard shortcut, and instruction text. When JavaScript adds the .active class to a drum pad, the CSS moves the pad slightly and adds a shadow and outline. This creates a visual animation whenever a drum sound is played.
:root {
--bg: #071026;
--card: #0b1220;
--accent: #fb7185;
--muted: #9aa4b2;
--white: #e6eef6;
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}
html,
body {
height: 100%;
}
body {
margin: 0;
background: #002252;
color: var(--white);
display: flex;
align-items: center;
justify-content: center;
padding: 28px;
}
.card {
width: min(860px, 96%);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
border-radius: 12px;
padding: 18px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.03);
}
header {
display: flex;
gap: 12px;
align-items: center;
}
.logo {
width: 44px;
height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), #22c1c3);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
h1 {
margin: 0;
font-size: 18px;
}
.lead {
margin: 4px 0 12px;
color: var(--muted);
font-size: 14px;
}
.kit {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-top: 12px;
}
@media (max-width:640px) {
.kit {
grid-template-columns: repeat(2, 1fr);
}
}
.pad {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.04);
padding: 18px;
border-radius: 12px;
text-align: center;
cursor: pointer;
user-select: none;
touch-action: none;
position: relative;
min-height: 110px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
transition: transform 120ms ease, box-shadow 120ms ease;
}
.pad .key {
font-family: ui-monospace, monospace;
font-weight: 700;
font-size: 18px;
color: var(--muted);
margin-top: 8px;
}
.pad .label {
font-size: 16px;
font-weight: 700;
color: #ffffff;
}
.pad:active,
.pad.active {
transform: translateY(-6px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
outline: 3px solid rgba(124, 58, 237, 0.12);
}
.pad .sub {
font-size: 13px;
color: var(--muted);
margin-top: 6px;
}
.legend {
margin-top: 8px;
color: var(--muted)
}
.small {
font-size: 13px;
color: var(--muted)
} Javascript Code
JavaScript handles the main functionality of the Drum Kit. First, the PADS array stores information about each drum sound, including its ID, display label, keyboard key, audio file name, sound type and the buildPads() function reads this array and dynamically creates a button for every drum pad. It also adds pointer events so users can play sounds by clicking on the pads, initAudio() function creates an AudioContext and attempts to load the available drum samples using fetch(). When an audio file loads successfully, it is stored as an AudioBuffer. If loading fails, the application tries to use an HTML audio element instead.
The playBuffer() function plays the available audio sample and adjusts its volume according to the velocity value. If no audio sample is available, the synthDrum() function generates a basic drum-like sound using the Web Audio API and triggerPad() function connects the sound playback with the visual animation. Finally, keyboard event listeners detect the keys A, S, D, F, G, H, J, and K and trigger the matching drum pad. The application builds the interface when the page loads and initializes audio after the user’s first interaction.
/* Day 11 — Drum Kit
Place sample files (recommended names) in a folder named `sounds/`:
sounds/kick.wav
sounds/snare.wav
sounds/hihat.wav
sounds/tom.wav
sounds/crash.wav
sounds/ride.wav
sounds/clap.wav
sounds/perc.wav
*/
// Mapping: each pad has an id, display label, keyboard key, and sample filename
const PADS = [
{ id: 'kick', label: 'Kick', key: 'a', file: 'kick.wav', type: 'kick' },
{ id: 'snare', label: 'Snare', key: 's', file: 'snare.wav', type: 'snare' },
{ id: 'hihat', label: 'Hi-Hat', key: 'd', file: 'hihat.wav', type: 'hat' },
{ id: 'tom', label: 'Tom', key: 'f', file: 'tom.wav', type: 'tom' },
{ id: 'crash', label: 'Crash', key: 'g', file: 'crash.wav', type: 'crash' },
{ id: 'ride', label: 'Ride', key: 'h', file: 'ride.wav', type: 'ride' },
{ id: 'clap', label: 'Clap', key: 'j', file: 'clap.wav', type: 'snare' },
{ id: 'perc', label: 'Perc', key: 'k', file: 'perc.wav', type: 'perc' }
];
// Elements
const kitEl = document.getElementById('kit');
// Web Audio context & buffers
let audioCtx = null;
const buffers = new Map(); // id -> AudioBuffer or null
const audioElements = new Map(); // id -> HTMLAudioElement fallback
const SOUND_PATH = 'sounds/'; // folder where sound files are kept
// Build pads dynamically in DOM
function buildPads() {
PADS.forEach(p => {
const pad = document.createElement('button');
pad.className = 'pad';
pad.id = 'pad-' + p.id;
pad.setAttribute('data-id', p.id);
pad.setAttribute(
'aria-label',
`${p.label} drum pad, key ${p.key.toUpperCase()}`
);
pad.innerHTML = `
<div class="label">${p.label}</div>
<div class="key">${p.key.toUpperCase()}</div>
<div class="sub">Click or press ${p.key.toUpperCase()}</div>
`;
kitEl.appendChild(pad);
// pointer events
pad.addEventListener('pointerdown', ev => {
ev.preventDefault();
const rect = pad.getBoundingClientRect();
const relY = Math.max(
0,
Math.min(1, (ev.clientY - rect.top) / rect.height)
);
const velocity = 1 - relY; // louder at top
triggerPad(p.id, velocity);
});
document.addEventListener('pointerup', () =>
pad.classList.remove('active')
);
});
}
// Initialize Web Audio & load sample files
async function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
audioCtx.master = audioCtx.createGain();
audioCtx.master.gain.value = 0.9;
audioCtx.master.connect(audioCtx.destination);
}
await Promise.all(
PADS.map(async p => {
const url = SOUND_PATH + p.file;
try {
const resp = await fetch(url, { cache: 'force-cache' });
if (!resp.ok) throw new Error('Not found');
const arrayBuffer = await resp.arrayBuffer();
const decoded = await audioCtx.decodeAudioData(arrayBuffer);
buffers.set(p.id, decoded);
} catch (err) {
buffers.set(p.id, null);
try {
const audio = new Audio(url);
audio.preload = 'auto';
audioElements.set(p.id, audio);
} catch {
audioElements.set(p.id, null);
}
}
})
);
}
// Play using AudioBuffer or fallback
function playBuffer(id, velocity = 1) {
const buf = buffers.get(id);
if (!audioCtx) return;
const now = audioCtx.currentTime;
if (buf) {
const src = audioCtx.createBufferSource();
src.buffer = buf;
const gain = audioCtx.createGain();
gain.gain.value = Math.max(0.08, Math.min(1, velocity));
src.connect(gain);
gain.connect(audioCtx.master);
src.start(now);
src.onended = () => {
try {
src.disconnect();
gain.disconnect();
} catch {}
};
} else {
const el = audioElements.get(id);
if (el) {
const clone = el.cloneNode();
clone.volume = Math.max(0.08, Math.min(1, velocity));
clone.play().catch(() => {});
} else {
synthDrum(id, velocity);
}
}
}
// Basic synthesized fallback for missing samples
function synthDrum(id, velocity = 1) {
if (!audioCtx) return;
const now = audioCtx.currentTime;
const type = (PADS.find(p => p.id === id) || {}).type || 'perc';
if (type === 'kick') {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = 'sine';
o.frequency.setValueAtTime(150, now);
o.frequency.exponentialRampToValueAtTime(40, now + 0.15);
g.gain.setValueAtTime(0.001, now);
g.gain.exponentialRampToValueAtTime(0.8 * velocity, now + 0.02);
g.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
o.connect(g);
g.connect(audioCtx.master);
o.start(now);
o.stop(now + 0.6);
} else if (type === 'snare' || type === 'perc') {
const bufferSize = audioCtx.sampleRate * 0.2;
const noise = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = noise.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] =
(Math.random() * 2 - 1) *
Math.exp(-i / (bufferSize * 0.05));
}
const src = audioCtx.createBufferSource();
src.buffer = noise;
const bp = audioCtx.createBiquadFilter();
bp.type = 'bandpass';
bp.frequency.value = 1800;
const g = audioCtx.createGain();
g.gain.setValueAtTime(0.001, now);
g.gain.exponentialRampToValueAtTime(0.8 * velocity, now + 0.005);
g.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
src.connect(bp);
bp.connect(g);
g.connect(audioCtx.master);
src.start(now);
src.stop(now + 0.2);
} else if (type === 'hat' || type === 'ride') {
const bufferSize = audioCtx.sampleRate * 0.06;
const noise = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = noise.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] =
(Math.random() * 2 - 1) *
Math.exp(-i / (bufferSize * 0.02));
}
const src = audioCtx.createBufferSource();
src.buffer = noise;
const hp = audioCtx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.value = 5000;
const g = audioCtx.createGain();
g.gain.setValueAtTime(0.001, now);
g.gain.exponentialRampToValueAtTime(0.8 * velocity, now + 0.002);
g.gain.exponentialRampToValueAtTime(0.001, now + 0.06);
src.connect(hp);
hp.connect(g);
g.connect(audioCtx.master);
src.start(now);
src.stop(now + 0.08);
} else {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = 'square';
o.frequency.value = 1000;
g.gain.value = 0.0001;
g.gain.exponentialRampToValueAtTime(0.6 * velocity, now + 0.001);
g.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
o.connect(g);
g.connect(audioCtx.master);
o.start(now);
o.stop(now + 0.1);
}
}
// Trigger pad + animation
function triggerPad(id, velocity = 1) {
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume().catch(() => {});
}
const padEl = document.getElementById('pad-' + id);
if (padEl) {
padEl.classList.add('active');
setTimeout(() => padEl.classList.remove('active'), 160);
}
playBuffer(id, velocity);
}
// Keyboard mappings
const keyToPad = {};
PADS.forEach(p => (keyToPad[p.key] = p.id));
const keysDown = new Set();
document.addEventListener('keydown', e => {
if (e.repeat) return;
const k = e.key.toLowerCase();
const padId = keyToPad[k];
if (padId) {
if (!audioCtx) {
initAudio().catch(() => {});
} else if (audioCtx.state === 'suspended') {
audioCtx.resume().catch(() => {});
}
const velocity = e.shiftKey ? 0.6 : 1;
triggerPad(padId, velocity);
keysDown.add(k);
e.preventDefault();
}
});
document.addEventListener('keyup', e => {
const k = e.key.toLowerCase();
keysDown.delete(k);
});
// Build UI on page load
buildPads();
// Auto init on first click anywhere
document.addEventListener('pointerdown', async function initOnce() {
document.removeEventListener('pointerdown', initOnce);
try {
await initAudio();
} catch (e) {
console.warn('Audio init failed, using synth fallback.', e);
}
});
// Debug
window.triggerPad = triggerPad;
Related Projects
Day 9 : Flashcard Learning App
Create and flip flashcards to study and test your knowledge.
Concepts: DOM manipulation, CSS 3D effects, LocalStorage.
Day 13 : Currency Converter
Converts one currency to another using real-time exchange rates.
Concepts: API fetch, DOM updates.
Day 14 : Image Gallery with Modal
Displays images in a grid with a pop-up modal view.
Concepts: DOM traversal, event bubbling.