Build notes / 08

Inside
CHORUS.

A small, working step sequencer for making something out of a little noise. The signature technique is web audio step-sequencer instrument.

01

Make every pad a control

Three voices by eight steps produce 24 native buttons. Each button has a unique accessible name and aria-pressed state. The visual matrix comes from CSS grid, not an image.

02

Synthesize instead of sampling

Each active step creates an oscillator and gain envelope. The voices use sine at 110 Hz, triangle at 330 Hz, and square at 880 Hz. Frequency falls to half over 120 ms, and gain decays to 0.001 in 180 ms.

03

Schedule ahead

A 25 ms JavaScript interval schedules audio 100 ms into the AudioContext clock. Eighth-note duration is 60 / BPM / 2. This avoids making audio timing depend on frame rate.

04

Respect intent and context

Audio starts only after Play. Stop, pagehide, and hidden-tab transitions suspend playback. Reduced motion removes the moving step indicator but does not disable user-requested sound. The gain is limited to 0.04 per voice.

Reproduce the site

  1. Download the complete source below and keep its name index.html.
  2. Open it through a static server. From its directory, run python3 -m http.server 8000, then visit http://localhost:8000. There is no install or build step.
  3. The stylesheet lives in the head. The rendering and interaction code lives in the final script. Adjust one parameter at a time and compare the result at desktop and 375 px widths.
  4. Google Fonts requires a network connection. All visible artwork is computed locally. Native system fallbacks preserve readable text if fonts fail.
  5. Enable your operating system’s reduced-motion setting and repeat the interaction. Use browser developer tools to check errors, overflow, and frame timing before publishing.
Download complete HTML ↓
The complete rendering and interaction code
let ac,playing=false,step=0,next=0,timer,bpm=108;const pads=[...document.querySelectorAll('.pad')],play=document.querySelector('#play'),rm=matchMedia('(prefers-reduced-motion: reduce)');pads.forEach(p=>p.onclick=()=>p.setAttribute('aria-pressed',String(p.getAttribute('aria-pressed')!=='true')));function voice(row,when){const o=ac.createOscillator(),g=ac.createGain();o.type=['sine','triangle','square'][row];const f=[110,330,880][row];o.frequency.setValueAtTime(f,when);o.frequency.exponentialRampToValueAtTime(f*.5,when+.12);g.gain.setValueAtTime(0,when);g.gain.linearRampToValueAtTime(.04,when+.005);g.gain.exponentialRampToValueAtTime(.001,when+.18);o.connect(g);g.connect(ac.destination);o.start(when);o.stop(when+.2)}function schedule(){while(next<ac.currentTime+.1){pads.filter(p=>+p.dataset.step===step&&p.getAttribute('aria-pressed')==='true').forEach(p=>voice(+p.dataset.row,next));const current=step;if(!rm.matches)setTimeout(()=>{if(playing)pads.forEach(p=>p.classList.toggle('current',+p.dataset.step===current))},Math.max(0,(next-ac.currentTime)*1000));step=(step+1)%8;next+=60/bpm/2}}function stop(){playing=false;clearInterval(timer);pads.forEach(p=>p.classList.remove('current'));play.setAttribute('aria-pressed','false');play.textContent='PLAY ▶';document.querySelector('#display').textContent='CHORUS-08 · STOPPED';if(ac)ac.suspend()}play.onclick=async()=>{if(playing){stop();return}ac??=new (window.AudioContext||window.webkitAudioContext)();await ac.resume();playing=true;step=0;next=ac.currentTime+.05;play.setAttribute('aria-pressed','true');play.textContent='STOP ■';document.querySelector('#display').textContent='CHORUS-08 · PLAYING';schedule();timer=setInterval(schedule,25)};document.querySelector('#tempo').oninput=e=>{bpm=+e.target.value;document.querySelector('#bpm').textContent=bpm};document.querySelector('#clear').onclick=()=>{pads.forEach(p=>p.setAttribute('aria-pressed','false'));document.querySelector('#display').textContent='CHORUS-08 · PATTERN CLEARED'};document.addEventListener('visibilitychange',()=>{if(document.hidden&&playing)stop()});addEventListener('pagehide',stop)
The complete visual system
:root{--body:'Chakra Petch',sans-serif}body{background:#17191b;color:#e4e2d8;font-family:var(--body);background-image:linear-gradient(#ffffff04 1px,transparent 1px);background-size:100% 4px}header{padding:25px 4vw;display:flex;justify-content:space-between;font-size:12px;border-bottom:1px solid #767674}header b{color:#ff4233;letter-spacing:.2em}.shell{max-width:1200px;margin:55px auto;padding:0 4vw}.title{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:35px}.title h1{font-size:clamp(62px,11vw,140px);font-weight:700;letter-spacing:-.07em;line-height:1;margin:0}.title p{font-size:14px;max-width:240px;color:#b3b4b0}.rack{background:#2d3032;border:1px solid #656762;border-radius:12px;padding:32px;box-shadow:0 18px 0 #080909,0 35px 70px #0008}.rack-top{display:flex;justify-content:space-between;align-items:center;gap:25px;margin-bottom:30px}.display{background:#090d0a;color:#b1db9b;box-shadow:inset 0 0 12px #000;flex:1;padding:20px;font-size:15px;letter-spacing:.1em}#play{background:#ff4233;color:#170c0a;border:0;border-radius:50%;width:80px;height:80px;font-weight:700;font-size:13px}#play[aria-pressed=true]{background:#b1db9b}.sequencer{display:grid;grid-template-columns:55px repeat(8,1fr);gap:10px}.sequencer label{font-size:12px;display:flex;align-items:center}.pad{height:55px;background:#191c1e;border:1px solid #5a5b57;border-radius:5px;box-shadow:0 4px 0 #111}.pad[aria-pressed=true]{background:#f44838;border-color:#ff8a71}.pad.current{outline:2px solid #fff;outline-offset:2px}.bottom-rack{display:flex;justify-content:space-between;gap:25px;align-items:center;margin-top:35px;font-size:14px}.bottom-rack input{accent-color:#ff4233;max-width:180px}.bottom-rack button{border:1px solid #85867e;background:transparent;padding:12px}.note{font-size:13px;color:#bec0b8;max-width:650px;margin-top:45px}.colophon{margin-top:75px;border-top:1px solid #656762}@media(max-width:650px){.shell{margin-top:30px}.title{display:block}.title h1{font-size:76px}.title p{max-width:none}.rack{padding:18px 12px}.rack-top{gap:12px}.display{font-size:12px;padding:14px;letter-spacing:0}#play{width:65px;height:65px;flex-shrink:0}.sequencer{grid-template-columns:35px repeat(8,1fr);gap:5px}.pad{height:40px}.sequencer label{font-size:12px}.bottom-rack{flex-wrap:wrap;font-size:13px}.bottom-rack input{max-width:140px}}

Original fictional design study. No stock photography, model files, image textures, or sample audio.