01. Click
The most common interaction. Fires once when the user releases the mouse button.
Preview index.html style.css script.js
< button class = "btn" id = "click-btn" >Click me</ button >
< div class = "output" id = "click-out" >No clicks yet</ div > .btn {
padding : .75 rem 1.5 rem ;
background : #6366f1 ;
color : white ;
border : none ;
border-radius : 8 px ;
font-size : 1 rem ;
cursor : pointer ;
}
.output {
margin-top : 1 rem ;
font-family : monospace ;
font-size : .875 rem ;
color : #64748b ;
} setTimeout (() => {
let count = 0 ;
const btn = document. getElementById ( "click-btn" );
const out = document. getElementById ( "click-out" );
if ( ! btn || ! out) return ;
btn. addEventListener ( "click" , () => {
count ++ ;
out.textContent = `Clicked ${ count } time${ count > 1 ? "s" : ""}` ;
});
}, 0 );
const btn = document. querySelector ( "#click-btn" )
btn. addEventListener ( "click" , () => {
// runs every time the user clicks
})
02. Double Click
dblclick fires only on two rapid clicks. Useful for toggles or zooming.
Preview index.html style.css script.js
< div class = "dbl-target" id = "dbl-box" >Double-click me</ div >
< div class = "out" id = "dbl-out" >Waiting for dblclick...</ div > .dbl-target {
width : 160 px ;
height : 80 px ;
background : #6366f1 ;
border-radius : 12 px ;
display : flex ;
align-items : center ;
justify-content : center ;
color : white ;
font-weight : 600 ;
cursor : pointer ;
user-select : none ;
transition : transform .15 s ;
}
.dbl-target.zoomed {
transform : scale ( 1.3 );
background : #4f46e5 ;
}
.out {
margin-top : .75 rem ;
font-family : monospace ;
font-size : .875 rem ;
color : #64748b ;
} setTimeout (() => {
const box = document. getElementById ( "dbl-box" );
const out = document. getElementById ( "dbl-out" );
if ( ! box || ! out) return ;
box. addEventListener ( "dblclick" , () => {
box.classList. toggle ( "zoomed" );
out.textContent = `dblclick fired - zoomed: ${ box . classList . contains ( "zoomed" ) }` ;
});
}, 0 );
btn. addEventListener ( "dblclick" , () => {
// runs only on two rapid clicks
})
03. Mouse Move
Fires continuously as the cursor moves. e.offsetX/e.offsetY give position relative to the element.
Preview index.html style.css script.js
< div class = "zone" id = "mouse-zone" >
Move your mouse here
< div class = "dot" id = "dot" ></ div >
</ div >
< div class = "output" id = "mouse-out" >x: - , y: -</ div > .zone {
width : 100 % ;
max-width : 360 px ;
height : 160 px ;
background : #f1f5f9 ;
border : 2 px dashed #cbd5e1 ;
border-radius : 12 px ;
display : flex ;
align-items : center ;
justify-content : center ;
font-size : .875 rem ;
color : #94a3b8 ;
cursor : crosshair ;
position : relative ;
}
.dot {
position : absolute ;
width : 10 px ;
height : 10 px ;
background : #6366f1 ;
border-radius : 50 % ;
transform : translate ( -50 % , -50 % );
pointer-events : none ;
display : none ;
}
.output {
margin-top : .75 rem ;
font-family : monospace ;
font-size : .875 rem ;
color : #64748b ;
} setTimeout (() => {
const zone = document. getElementById ( "mouse-zone" );
const dot = document. getElementById ( "dot" );
const out = document. getElementById ( "mouse-out" );
if ( ! zone || ! dot || ! out) return ;
zone. addEventListener ( "mousemove" , ( e ) => {
const rect = zone. getBoundingClientRect ();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
dot.style.display = "block" ;
dot.style.left = x + "px" ;
dot.style.top = y + "px" ;
out.textContent = `x: ${ Math . round ( x ) } , y: ${ Math . round ( y ) }` ;
});
zone. addEventListener ( "mouseleave" , () => {
dot.style.display = "none" ;
});
}, 0 );
area. addEventListener ( "mousemove" , ( e ) => {
const x = e.offsetX
const y = e.offsetY
const hue = Math. round ((x / area.offsetWidth) * 360 )
area.style.background = `hsl(${ hue }, 65%, 50%)`
})
04. Keyboard
keydown fires on press, keyup on release. e.key gives you the character name.
Preview index.html style.css script.js
< input class = "input" id = "key-input" placeholder = "Type something..." />
< div class = "output" id = "key-out" >Last key: -</ div > .input {
padding : .6 rem 1 rem ;
border : 2 px solid #e2e8f0 ;
border-radius : 8 px ;
font-size : 1 rem ;
width : 100 % ;
max-width : 320 px ;
outline : none ;
transition : border-color .15 s ;
}
.input:focus {
border-color : #6366f1 ;
}
.output {
margin-top : 1 rem ;
font-family : monospace ;
font-size : .875 rem ;
color : #64748b ;
} setTimeout (() => {
const input = document. getElementById ( "key-input" );
const out = document. getElementById ( "key-out" );
if ( ! input || ! out) return ;
input. addEventListener ( "keydown" , ( e ) => {
out.textContent = `Last key: "${ e . key }" (code: ${ e . code })` ;
});
}, 0 );
document. addEventListener ( "keydown" , ( e ) => {
console. log (e.key) // "a", "Enter", "ArrowUp", " " (space)…
display.classList. add ( "active" )
})
document. addEventListener ( "keyup" , () => {
display.classList. remove ( "active" )
})
05. Drag & Drop
Add draggable="true" to any element. You must call e.preventDefault() in dragover or drop will never fire.
Preview index.html style.css script.js
< div class = "dnd-wrap" >
< div class = "dnd-zone" id = "dnd-a" >
< div class = "dnd-item" draggable = "true" id = "dnd-item" >Drag me - > </ div >
</ div >
< div class = "dnd-zone" id = "dnd-b" ></ div >
</ div >
< div class = "out" id = "dnd-out" >Drag the item between the two zones</ div > .dnd-wrap {
display : flex ;
gap : 1 rem ;
align-items : flex-start ;
}
.dnd-zone {
flex : 1 ;
min-height : 80 px ;
background : #f1f5f9 ;
border : 2 px dashed #cbd5e1 ;
border-radius : 8 px ;
padding : .5 rem ;
transition : border-color .15 s , background .15 s ;
}
.dnd-zone.over {
border-color : #6366f1 ;
background : #ede9fe ;
}
.dnd-item {
padding : .5 rem 1 rem ;
background : #6366f1 ;
color : white ;
border-radius : 6 px ;
cursor : grab ;
font-size : .875 rem ;
font-weight : 600 ;
display : inline-block ;
}
.dnd-item:active {
cursor : grabbing ;
}
.out {
margin-top : .75 rem ;
font-family : monospace ;
font-size : .8 rem ;
color : #64748b ;
} setTimeout (() => {
const item = document. getElementById ( "dnd-item" );
const zoneA = document. getElementById ( "dnd-a" );
const zoneB = document. getElementById ( "dnd-b" );
const out = document. getElementById ( "dnd-out" );
if ( ! item || ! zoneA || ! zoneB || ! out) return ;
item. addEventListener ( "dragstart" , ( e ) => {
e.dataTransfer. setData ( "text/plain" , "dnd-item" );
});
[zoneA, zoneB]. forEach (( zone ) => {
zone. addEventListener ( "dragover" , ( e ) => {
e. preventDefault ();
zone.classList. add ( "over" );
});
zone. addEventListener ( "dragleave" , () => zone.classList. remove ( "over" ));
zone. addEventListener ( "drop" , ( e ) => {
e. preventDefault ();
zone.classList. remove ( "over" );
zone. appendChild (item);
out.textContent = `Dropped in ${ zone . id === "dnd-a" ? "Zone A" : "Zone B"}` ;
});
});
}, 0 );
item. addEventListener ( "dragstart" , ( e ) => {
e.dataTransfer. setData ( "text/plain" , item.id)
})
target. addEventListener ( "dragover" , ( e ) => {
e. preventDefault () // required — without this, drop never fires
})
target. addEventListener ( "drop" , ( e ) => {
e. preventDefault ()
const id = e.dataTransfer. getData ( "text/plain" )
target. appendChild (document. getElementById (id))
})
06. Window Resize
Fires when the browser window changes size. Useful for responsive canvas or layout recalculations.
Preview index.html style.css script.js
< div class = "wr-display" id = "wr-size" >- × -</ div >
< div class = "wr-label" >window.innerWidth × window.innerHeight (resize the panel)</ div >
< div class = "wr-bar-wrap" >< div class = "wr-bar" id = "wr-bar" style = "width:50%" ></ div ></ div > .wr-display {
font-family : monospace ;
font-size : 2 rem ;
font-weight : 700 ;
color : #6366f1 ;
}
.wr-label {
font-size : .8 rem ;
color : #94a3b8 ;
margin-top : .25 rem ;
}
.wr-bar-wrap {
margin-top : .75 rem ;
height : 8 px ;
background : #e2e8f0 ;
border-radius : 4 px ;
}
.wr-bar {
height : 100 % ;
background : #6366f1 ;
border-radius : 4 px ;
transition : width .3 s ;
} setTimeout (() => {
const sizeEl = document. getElementById ( "wr-size" );
const barEl = document. getElementById ( "wr-bar" );
if ( ! sizeEl || ! barEl) return ;
const update = () => {
const w = window.innerWidth;
const h = window.innerHeight;
sizeEl.textContent = `${ w } × ${ h }` ;
barEl.style.width = Math. min ( 100 , (w / 1440 ) * 100 ) + "%" ;
};
window. addEventListener ( "resize" , update);
update ();
}, 0 );
window. addEventListener ( "resize" , () => {
console. log (window.innerWidth, window.innerHeight)
})
07. Camera
navigator.mediaDevices.getUserMedia() requests webcam access. Requires HTTPS or localhost.
Preview index.html style.css script.js
< video id = "cam-video" autoplay playsinline muted ></ video >
< div class = "cam-status" id = "cam-status" >Camera not started</ div >
< div class = "btns" >
< button class = "btn btn-start" id = "cam-start" >Request camera</ button >
< button class = "btn btn-stop" id = "cam-stop" >Stop</ button >
</ div > video {
width : 100 % ;
max-width : 320 px ;
border-radius : 8 px ;
background : #0f172a ;
display : block ;
}
.cam-status {
font-size : .8 rem ;
color : #64748b ;
margin : .5 rem 0 ;
}
.btns {
display : flex ;
gap : .5 rem ;
}
.btn {
padding : .4 rem .9 rem ;
border : none ;
border-radius : 6 px ;
cursor : pointer ;
font-size : .8 rem ;
}
.btn-start {
background : #6366f1 ;
color : white ;
}
.btn-stop {
background : white ;
color : #6366f1 ;
border : 1 px solid #6366f1 ;
} setTimeout (() => {
const video = document. getElementById ( "cam-video" );
const status = document. getElementById ( "cam-status" );
const startBtn = document. getElementById ( "cam-start" );
const stopBtn = document. getElementById ( "cam-stop" );
if ( ! video || ! status || ! startBtn || ! stopBtn) return ;
let stream = null ;
startBtn. onclick = async () => {
try {
stream = await navigator.mediaDevices. getUserMedia ({ video: true });
video.srcObject = stream;
status.textContent = "Camera active" ;
} catch (e) {
status.textContent = "Error: " + e.message;
}
};
stopBtn. onclick = () => {
if (stream) {
stream. getTracks (). forEach (( t ) => t. stop ());
video.srcObject = null ;
}
status.textContent = "Camera stopped" ;
};
}, 0 );
const stream = await navigator.mediaDevices. getUserMedia ({ video: true })
videoEl.srcObject = stream
// turn the camera off
stream. getTracks (). forEach ( track => track. stop ())
08. Microphone
AudioContext lets you analyse sound in real time. requestAnimationFrame redraws the meter ~60fps.
Preview index.html style.css script.js
< div class = "mic-status" id = "mic-status" >Microphone not started</ div >
< div class = "mic-meter-wrap" >< div class = "mic-meter" id = "mic-meter" ></ div ></ div >
< div class = "btns" >
< button class = "btn btn-start" id = "mic-start" >Request mic</ button >
< button class = "btn btn-stop" id = "mic-stop" >Stop</ button >
</ div > .mic-meter-wrap {
background : #f1f5f9 ;
border-radius : 99 px ;
height : 1.5 rem ;
margin : .75 rem 0 ;
overflow : hidden ;
}
.mic-meter {
height : 100 % ;
background : #6366f1 ;
border-radius : 99 px ;
width : 0 ;
transition : width .08 s ;
}
.mic-status {
font-size : .8 rem ;
color : #64748b ;
}
.btns {
display : flex ;
gap : .5 rem ;
}
.btn {
padding : .4 rem .9 rem ;
border : none ;
border-radius : 6 px ;
cursor : pointer ;
font-size : .8 rem ;
}
.btn-start {
background : #6366f1 ;
color : white ;
}
.btn-stop {
background : white ;
color : #6366f1 ;
border : 1 px solid #6366f1 ;
} setTimeout (() => {
const meter = document. getElementById ( "mic-meter" );
const status = document. getElementById ( "mic-status" );
const startBtn = document. getElementById ( "mic-start" );
const stopBtn = document. getElementById ( "mic-stop" );
if ( ! meter || ! status || ! startBtn || ! stopBtn) return ;
let audioCtx = null ;
let analyser = null ;
let rafId = null ;
let stream = null ;
startBtn. onclick = async () => {
try {
stream = await navigator.mediaDevices. getUserMedia ({ audio: true });
audioCtx = new AudioContext ();
analyser = audioCtx. createAnalyser ();
audioCtx. createMediaStreamSource (stream). connect (analyser);
const data = new Uint8Array (analyser.frequencyBinCount);
status.textContent = "Listening..." ;
const draw = () => {
analyser. getByteFrequencyData (data);
const avg = data. reduce (( a , b ) => a + b, 0 ) / data. length ;
meter.style.width = Math. min ( 100 , (avg / 128 ) * 100 ) + "%" ;
rafId = requestAnimationFrame (draw);
};
draw ();
} catch (e) {
status.textContent = "Error: " + e.message;
}
};
stopBtn. onclick = () => {
cancelAnimationFrame (rafId);
if (stream) stream. getTracks (). forEach (( t ) => t. stop ());
if (audioCtx) audioCtx. close ();
meter.style.width = "0" ;
status.textContent = "Stopped" ;
};
}, 0 );
const stream = await navigator.mediaDevices. getUserMedia ({ audio: true })
const ctx = new AudioContext ()
const source = ctx. createMediaStreamSource (stream)
const analyser = ctx. createAnalyser ()
source. connect (analyser)
const data = new Uint8Array (analyser.frequencyBinCount)
function draw () {
analyser. getByteFrequencyData (data)
const avg = data. reduce (( a , b ) => a + b, 0 ) / data. length
meter.style.width = (avg / 255 * 100 ) + "%"
requestAnimationFrame (draw)
}
draw ()
09. Touch
Works on phones, tablets, and drawing tablets. e.touches is a list — one entry per active finger.
Preview index.html style.css script.js
< div class = "touch-area" id = "touch-area" >
< div class = "touch-dot" id = "touch-dot" ></ div >
</ div >
< div class = "out" id = "touch-out" >Touch the area above</ div >
< p class = "hint" >Works on mobile/tablet. On desktop, mouse events fire instead.</ p > .touch-area {
width : 100 % ;
height : 160 px ;
background : #f1f5f9 ;
border-radius : 12 px ;
position : relative ;
touch-action : none ;
overflow : hidden ;
border : 2 px dashed #e2e8f0 ;
}
.touch-dot {
position : absolute ;
width : 40 px ;
height : 40 px ;
border-radius : 50 % ;
background : #6366f1 ;
opacity : 0 ;
transform : translate ( -50 % , -50 % );
pointer-events : none ;
transition : opacity .15 s ;
}
.out {
margin-top : .5 rem ;
font-family : monospace ;
font-size : .8 rem ;
color : #64748b ;
}
.hint {
font-size : .75 rem ;
color : #94a3b8 ;
margin-top : .25 rem ;
} setTimeout (() => {
const area = document. getElementById ( "touch-area" );
const dot = document. getElementById ( "touch-dot" );
const out = document. getElementById ( "touch-out" );
if ( ! area || ! dot || ! out) return ;
const showDot = ( x , y ) => {
const rect = area. getBoundingClientRect ();
dot.style.left = x - rect.left + "px" ;
dot.style.top = y - rect.top + "px" ;
dot.style.opacity = "0.8" ;
};
area. addEventListener ( "touchstart" , ( e ) => {
e. preventDefault ();
const t = e.touches[ 0 ];
showDot (t.clientX, t.clientY);
out.textContent = `touches: ${ e . touches . length } | x: ${ Math . round ( t . clientX ) }, y: ${ Math . round ( t . clientY ) }` ;
}, { passive: false });
area. addEventListener ( "touchmove" , ( e ) => {
e. preventDefault ();
const t = e.touches[ 0 ];
showDot (t.clientX, t.clientY);
out.textContent = `moving - touches: ${ e . touches . length } | x: ${ Math . round ( t . clientX ) }, y: ${ Math . round ( t . clientY ) }` ;
}, { passive: false });
area. addEventListener ( "touchend" , () => {
dot.style.opacity = "0" ;
out.textContent = "touchend fired" ;
});
area. addEventListener ( "mousemove" , ( e ) => {
showDot (e.clientX, e.clientY);
out.textContent = `mousemove (desktop) - x: ${ e . offsetX }, y: ${ e . offsetY }` ;
});
area. addEventListener ( "mouseleave" , () => {
dot.style.opacity = "0" ;
});
}, 0 );
area. addEventListener ( "touchstart" , ( e ) => {
console. log (e.touches. length ) // number of fingers
})
area. addEventListener ( "touchmove" , ( e ) => {
e. preventDefault ()
const t = e.touches[ 0 ]
console. log (t.clientX, t.clientY)
}, { passive: false })
area. addEventListener ( "touchend" , () => {
// all fingers lifted
})
10. Clipboard
Copy text with navigator.clipboard.writeText(). Listen for paste to read what the user pastes. Requires HTTPS or localhost.
Preview index.html style.css script.js
< div class = "cb-demo" >
< div class = "section" >
< div class = "sec-label" >navigator.clipboard.writeText()</ div >
< div class = "row" >
< span class = "code-chip" id = "cb-text" >hsl(247, 70%, 55%)</ span >
< button class = "btn" id = "cb-copy" >Copy</ button >
</ div >
< div class = "out" id = "cb-copy-out" >-</ div >
</ div >
< div class = "section" >
< div class = "sec-label" >paste event - paste into the field below</ div >
< input class = "paste-target" id = "cb-paste-target" placeholder = "Paste here (Cmd/Ctrl+V)..." />
< div class = "out" id = "cb-paste-out" >-</ div >
</ div >
</ div > .cb-demo {
display : flex ;
flex-direction : column ;
gap : .75 rem ;
}
.section {
background : #f8fafc ;
border-radius : 8 px ;
padding : .6 rem .9 rem ;
}
.sec-label {
font-size : .75 rem ;
font-family : monospace ;
font-weight : 600 ;
color : #64748b ;
margin-bottom : .4 rem ;
}
.row {
display : flex ;
gap : .5 rem ;
align-items : center ;
}
.code-chip {
font-family : monospace ;
font-size : .875 rem ;
background : #e0e7ff ;
padding : .3 rem .6 rem ;
border-radius : 4 px ;
color : #3730a3 ;
}
.btn {
padding : .35 rem .75 rem ;
background : #6366f1 ;
color : white ;
border : none ;
border-radius : 6 px ;
cursor : pointer ;
font-size : .8 rem ;
}
.out {
font-family : monospace ;
font-size : .8 rem ;
margin-top : .4 rem ;
color : #64748b ;
}
.paste-target {
padding : .4 rem .75 rem ;
border : 1 px solid #e2e8f0 ;
border-radius : 6 px ;
font-family : monospace ;
font-size : .8 rem ;
width : 100 % ;
} setTimeout (() => {
const copyOut = document. getElementById ( "cb-copy-out" );
const pasteOut = document. getElementById ( "cb-paste-out" );
const text = document. getElementById ( "cb-text" );
const copyBtn = document. getElementById ( "cb-copy" );
const pasteTarget = document. getElementById ( "cb-paste-target" );
if ( ! copyOut || ! pasteOut || ! text || ! copyBtn || ! pasteTarget) return ;
const fallbackCopy = ( value ) => {
const ta = document. createElement ( "textarea" );
ta.value = value;
ta. setAttribute ( "readonly" , "" );
ta.style.position = "fixed" ;
ta.style.opacity = "0" ;
ta.style.pointerEvents = "none" ;
document.body. appendChild (ta);
ta. select ();
const ok = document. execCommand ( "copy" );
document.body. removeChild (ta);
return ok;
};
copyBtn. onclick = async () => {
const value = text.textContent;
try {
if ( ! navigator.clipboard?.writeText) {
const ok = fallbackCopy (value);
copyOut.textContent = ok
? "Copied with fallback (execCommand)."
: "Copy failed - use Cmd/Ctrl+C manually." ;
return ;
}
await navigator.clipboard. writeText (value);
copyOut.textContent = "Copied! Now paste it somewhere." ;
} catch (e) {
const blocked = / permissions policy | NotAllowedError | blocked / i . test ( String (e?.message || e));
if (blocked) {
const ok = fallbackCopy (value);
copyOut.textContent = ok
? "Clipboard API blocked, copied with fallback (execCommand)."
: "Clipboard blocked by policy. Use Cmd/Ctrl+C manually." ;
return ;
}
copyOut.textContent = "Error: " + e.message;
}
};
pasteTarget. addEventListener ( "paste" , ( e ) => {
const pasted = e.clipboardData. getData ( "text/plain" );
pasteOut.textContent = `pasted: "${ pasted }"` ;
});
}, 0 );
await navigator.clipboard. writeText ( "Hello!" )
document. addEventListener ( "paste" , ( e ) => {
const text = e.clipboardData. getData ( "text/plain" )
console. log (text)
})
Cleaning up your events
It is often neglected to check whether events you setup are correctly cleaned up. By this, it means they are properly destroyed once not used anymore. When switching page, if you check your browser dev tools memory tab, you can see listeners climbing up as “detached”. This leaks memory.
Above we saw how to fire event listeners, let’s see how to clean them up, multiple ways:
Manually one by one:
When removing manually a single event listener, both the event and passed function need to be identical for this to work.
This approach is good when having very little amount of event listeners, but not good when having dozens.
Preview script.js
el. addEventListener ( 'click' , onClick);
//later, to remove it:
el. removeEventListener ( 'click' , onClick);
Batch – with AbortController
The AbortController helps cleanup all events at once.
AbortController gives access to a signal acting as a one-off switch.
We can then pass this signal as third parameter (the options) of our event listeners:
Preview script.js
const controller = new AbortController ();
const { signal } = controller // one off switch
el. addEventListener ( 'click' , onClick, { signal })
el. addEventListener ( 'scroll' , onClick, { signal })
controller. abort ();
MDN: AbortController