Virtual Piano Keyboard — Free HTML CSS JS Snippet

Virtual Piano Keyboard · Buttons · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real Web Audio synthesis: OscillatorNode at true equal-tempered frequencies, no sample files
GainNode envelope: linearRampToValueAtTime attack (20ms) and exponential/linear decay avoid click artifacts
CSS-only piano layout: absolute-positioned black keys computed at (i+1)*14.29%-halfWidth over a flex white-key row
Triple input support: mousedown/up, touchstart/end, and keydown/up all drive the same noteOn/noteOff functions
e.repeat guard prevents keyboard auto-repeat from retriggering a held note
Shared active{} note map prevents duplicate oscillators and correctly tracks sustain per note
Pressed-key animation: background gradient shift + translateY(3px) dip + inset shadow on .active
window blur listener releases all held notes if focus leaves the page mid-press, preventing stuck drones

About this UI Snippet

Virtual Piano Keyboard — Web Audio Oscillator Synthesis with CSS-Positioned Black Keys

Screenshot of the Virtual Piano Keyboard snippet rendered live

Most "piano" demos on the web are static images or, at best, buttons that toggle a colour with no actual sound. This snippet is a genuine playable instrument: seven white keys and five black keys spanning one octave (C4 through C5), each producing a real musical tone generated live in the browser via the Web Audio API — no audio files, no samples, no external libraries.

How the keyboard layout is built in pure CSS

A real piano's black keys don't sit between white keys edge to edge — they overlap, floating above the boundary between two adjacent white keys, and two of the seven white-key gaps (E-F and B-C) have no black key at all. This snippet reproduces that exactly. The white keys are a simple display: flex row where each .key.white takes an equal flex: 1 share of the container. The black keys live in a separate .black-keys layer with position: absolute; top: 0; height: 58%, and each individual black key is positioned with an explicit left percentage computed from the white-key grid: for seven equal keys each spanning 100/7 ≈ 14.29% of the width, a black key sitting on the boundary after white key index i is centred at (i+1) * 14.29% - halfBlackWidth. That arithmetic is baked into the five left values in the HTML (10.29%, 24.57%, 53.14%, 67.43%, 81.71%), which is why C#, D#, F#, G#, and A# land in exactly the right visual gaps and the E-F / B-C gaps stay open, matching a real keyboard.

How the sound is actually synthesized

Every key press calls noteOn(note, freq, keyEl), which lazily creates a single shared AudioContext (browsers require it to originate from a user gesture, so it's only instantiated on first interaction and resumed if suspended). For each note it builds an OscillatorNode set to type: 'triangle' — a triangle wave gives a softer, more piano-like timbre than the harsh default sine or buzzy square/sawtooth — tuned to the note's real equal-tempered frequency (C4 = 261.63 Hz, up through B4 = 493.88 Hz, using standard concert pitch where A4 = 440 Hz). The oscillator is routed through a dedicated GainNode that implements a simple attack/sustain envelope: gain.gain.linearRampToValueAtTime(0.32, now + 0.02) ramps volume up over 20 ms to avoid the sharp audible "click" a hard setValueAtTime jump would cause, then exponentialRampToValueAtTime(0.16, now + 0.35) eases into a sustain level so held notes don't stay at full volume indefinitely. Releasing the key (mouseup, touchend, or keyup) calls noteOff, which ramps gain down to near-zero over 400 ms with another linearRampToValueAtTime before calling osc.stop(), producing a natural decay instead of an abrupt cutoff.

Why this is genuinely educational

This snippet is a compact demonstration of three separate front-end skills at once: percentage-based absolute positioning to reconstruct a real-world physical layout in CSS, the Web Audio node graph (oscillator → gain → destination) as the standard pattern for programmatic sound synthesis, and dual input handling that keeps mouse, touch, and keyboard interactions perfectly in sync through a single shared active note-tracking object keyed by note name — which also elegantly solves the "don't retrigger a note that's already sounding" problem when a held keyboard key fires repeated keydown events.

Why it's fun to play with

Because every note is a real, correctly-tuned pitch rather than a placeholder beep, you can actually play recognisable melodies on it — and the pressed-key animation (a colour shift plus a 3px translateY "dip" with an inset shadow to simulate the key physically depressing) gives the same tactile feedback loop that makes real electronic keyboards satisfying to noodle on.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to trace exactly how noteOn() and noteOff() build and tear down the oscillator/gain node graph for a single key press, and why the gain envelope uses a linear ramp for attack but a mix of exponential and linear ramps for decay. It's also a great starting point to ask the assistant to extend: request a second full octave with correctly recalculated black-key positioning percentages, a sustain pedal toggle that keeps notes ringing after keyup, or a simple recording feature that logs each noteOn call's timestamp and note name so a melody can be played back later. You could also ask it to add a visual waveform or frequency-spectrum display using an AnalyserNode tapped off the shared audio graph, which is a natural next step once the oscillator routing is understood.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a playable one-octave virtual piano in plain HTML, CSS, and JavaScript using the Web Audio API for real synthesized tones — no audio files or external libraries.

Requirements:
- Seven white keys (C D E F G A B) laid out with CSS flexbox, and five black keys (C# D# F# G# A#) absolutely positioned to overlap the correct boundaries between white keys, leaving no black key between E-F and B-C exactly like a real piano.
- Each key must be playable by mouse/touch (press and release) AND by a mapped computer keyboard letter, with the mapped letter shown as a small visible label on the key.
- Pressing a key must create an OscillatorNode tuned to that note's real equal-tempered frequency in Hz, routed through a GainNode with a fast linear attack ramp and a slower decay/release ramp on key-up, so notes never click or cut off abruptly.
- Holding a keyboard key down must sustain the note without retriggering on the browser's automatic key-repeat events, and releasing it must stop the note cleanly.
- Multiple keys pressed at once (mouse plus keyboard, or several keydown events) must be able to sound together as a chord, each with its own independent oscillator.
- Give pressed keys a clear visual "depressed" state (color change plus a small downward shift) that is removed exactly when the note stops.
- Handle the edge case where the browser window loses focus while a key is still held down, ensuring no note gets stuck playing forever.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Play a note with your mouse or fingerClick and hold any white or black key to sound its note — the key visually dips down and changes colour while the tone plays. Release the mouse or lift your finger (mouseup, touchend) to stop the note, which triggers the gain envelope's release ramp in noteOff().
  2. 2
    Play with your computer keyboardPress A S D F G H J for the seven white keys (C D E F G A B) and W E T Y U for the five black keys (C# D# F# G# A#), matching the small letter labels printed on each key. Holding a key down sustains the note; the e.repeat check in the keydown listener prevents the browser's key-repeat events from re-triggering noteOn() and cutting off the sustain.
  3. 3
    Understand the frequency valuesEach key element carries a data-freq attribute with its real equal-tempered frequency in Hz (e.g. data-freq="261.63" for C4). These come from the standard 12-tone equal temperament formula freq = 440 * 2^((n-49)/12), where n is the key's position on an 88-key piano and 440 Hz is A4 — you can verify any value by running that formula for the corresponding key number.
  4. 4
    Change the timbre or extend the rangeIn the JS panel, change osc.type from 'triangle' to 'sine' for a purer, softer tone, 'sawtooth' for a brighter synth-lead sound, or 'square' for a retro chiptune feel. To add a second octave, duplicate the .key.white and .key.black HTML blocks with new data-note and data-freq values (C5 = 523.25 Hz onward) and extend the black-key left percentages using the same (i+1)*14.29%-halfWidth formula.
  5. 5
    Add a visible note name displayAdd a <div id="now-playing"> above the piano, and inside noteOn() set now-playing.textContent = note.replace('4', '') to show which note is currently sounding. This is useful if you want to turn the piano into a simple ear-training or note-recognition tool.
  6. 6
    Export and add to your projectClick HTML to download a standalone file, or JSX to get a React component. In React, wrap the AudioContext creation in a ref (useRef(null)) instead of a module-level variable so it survives re-renders correctly, and initialise it lazily inside the first user-gesture handler exactly as this snippet does, since browsers block audio contexts created before any interaction.

Real-world uses

Common Use Cases

Teaching the Web Audio API node graph with a tangible result
Most Web Audio tutorials produce an abstract beep with no visual feedback. This snippet pairs the oscillator → gain → destination signal chain with a real, playable UI so the connection between the code and the sound is immediate and obvious. It is a strong reference implementation for anyone learning AudioContext, OscillatorNode, and GainNode envelope shaping for the first time.
Embeddable music education or ear-training widget
Because every key maps to a verifiably correct frequency, this piano can anchor simple music theory tools — interval recognition quizzes, chord-building demos, or a scale visualizer that highlights which keys belong to a given key signature. Add a "play this note" prompt and check whether the user presses the matching key.
A playful, sound-reactive hero or 404 page element
A working instrument is a memorable interaction for a portfolio, music-software landing page, or app that needs a moment of delight. Swap the accent colours in .key.white.active and .key.black.active to match your brand, and consider pairing it with the Bubble Wrap Popper for a page full of small satisfying interactions.
Reference for frequency-to-note math in any audio project
The comment-documented frequency values and the equal-temperament formula (440 * 2^((n-49)/12)) referenced in the howToUse section serve as a copy-pasteable reference for any project that needs to convert MIDI note numbers or note names to playback frequency, such as a sequencer, tuner, or synthesizer UI.
Accessibility and input-parity demonstration
The same noteOn/noteOff pair is invoked identically from mouse, touch, and keyboard handlers, which is a clean pattern to study for building any interactive control that must work correctly across pointer and keyboard users without duplicating logic three times.

Got questions?

Frequently Asked Questions

Browsers require an AudioContext to be created or resumed inside a direct user-gesture handler (click, touch, or keydown), which is why getCtx() lazily instantiates the context on the very first noteOn() call rather than at page load, and calls audioCtx.resume() if the browser auto-suspended it. If you still hear nothing, check that your browser tab isn't muted and that the system output volume is up — some browsers also block autoplay-adjacent audio contexts until a click occurs anywhere on the page first.

They use standard 12-tone equal temperament: freq = 440 * 2^((n-49)/12), where 440 Hz is the reference pitch A4 and n is the note's position on a standard 88-key piano (A4 is key 49). Plugging in n for C4 through B4 yields the exact values baked into each key's data-freq attribute (261.63 Hz through 493.88 Hz), which is the same tuning system used by every standard acoustic and digital piano.

Yes — duplicate the .key.white and .key.black blocks in the HTML with new note names, frequencies (multiply or divide by 2 to shift a full octave, e.g. C5 = 523.25 Hz), and keyboard letters, then extend .white-keys to hold more flex items and recompute the black-key left percentages using 100/totalWhiteKeys per key width. The JS requires no changes since it reads data-note, data-freq, and data-key generically from whatever elements exist.

A pure sine wave sounds like a plain electronic tone with no harmonic content, while a triangle wave has odd harmonics that roll off quickly, giving it a softer, slightly richer character closer to an acoustic instrument without the buzzy edge of a sawtooth or square wave. It's a common quick approximation used in simple synthesizers when a full sampled piano isn't available.

Yes — each note gets its own independent OscillatorNode and GainNode routed to the same AudioContext destination, so pressing several keys simultaneously (with a mouse plus keyboard, or multiple simultaneous keydown events) mixes all of their signals together automatically, which is exactly how you would play a chord on this virtual keyboard.