JavaScript in the browser: DOM, events, async
Ten snippets for browser-side work: debounce and throttle, detecting editable event targets, smooth anchor scrolling, lazy-loading autoplaying video, merging Tailwind classes, contrast decisions, and reading a JWT payload.
Browser code fails in ways server code does not: the user scrolls during your animation, types faster than your handler, opens the page with an anchor already in the URL, or has autoplay blocked. These ten snippets each handle one of those situations.
Debounce and throttle are not interchangeable
Both limit how often a function runs, and they are routinely swapped by mistake. Debounce waits for a pause — it is what you want for a search box, because you care about the final value, not the intermediate ones. Throttle runs at a fixed maximum rate — it is what you want for a scroll or resize handler, because you need periodic updates and never getting one is worse than getting a stale one. Using debounce on scroll produces a UI that updates only after the user stops, which usually reads as broken.
Keyboard shortcuts and the editable-target problem
Any app with single-key shortcuts eventually fires one while the user is typing in a form. Checking event.target.tagName === 'INPUT' is the usual first attempt and misses textareas, contenteditable regions, and elements inside a shadow root. The check needs to cover all three, and it is the difference between a shortcut system that feels considered and one that eats characters.
Autoplay is a permission, not a setting
Browsers block autoplaying video with sound, and muted autoplay still costs bandwidth and decoding on every video in the document. Attaching playback to an IntersectionObserver means a video starts only when it is actually on screen and stops when it leaves — which is both the polite behaviour and the fast one. The MDN reference on autoplay documents which combinations current browsers permit.
Reading a JWT is not verifying one
The payload snippet decodes base64url and returns the claims. That is all it does, and the distinction matters: anyone can craft a token whose payload says whatever they want. Decoding client-side is fine for deciding what to render; it is never a basis for deciding what to allow. Verification requires the signing key and belongs on the server.
Colour and class-merge helpers
Deciding whether to put white or black text on an arbitrary background is a perceived-luminance calculation, not a lightness one — the naive (r+g+b)/3 version gets green badly wrong. And merging Tailwind classes is not string concatenation: px-2 px-4 leaves both in the class list and lets specificity order decide, which is why a dedicated merge helper exists.
Merge Tailwind classes safely (cn helper)
Combines conditional classes with clsx and resolves conflicting Tailwind utilities with tailwind-merge — the standard "cn()" helper used across shadcn/ui-style codebases.
How it works: clsx collapses an array of strings/objects/falsy values into one class string; twMerge then resolves Tailwind-specific conflicts (e.g. px-2 vs px-4) by keeping only the last one that wins, instead of leaving both in the DOM. Requires npm install clsx tailwind-merge.
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs) {
return twMerge(clsx(inputs))
}
// Example usage — later classes win over earlier conflicting ones
cn('px-2 py-1', condition && 'px-4', 'text-sm')
// condition = true -> "py-1 px-4 text-sm"
// condition = false -> "px-2 py-1 text-sm"
Debounce a function
Delay a function's execution until a burst of calls has stopped — ideal for search inputs and resize handlers.
How it works:
- Each call to the debounced function clears the previous pending timer.
- A new timer is scheduled for
delayms. - The wrapped function only actually runs once calls stop arriving for
delayms.
function debounce(fn, delay = 300) {
let timeoutId
return function debounced(...args) {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => fn.apply(this, args), delay)
}
}
// Example usage
const onResize = debounce(() => console.log('window resized'), 250)
window.addEventListener('resize', onResize)
Throttle a function
Ensures a function runs at most once every X milliseconds — ideal for scroll and mousemove handlers that would otherwise fire hundreds of times a second.
How it works: unlike debounce (which waits for calls to stop before running), throttle guarantees the function still runs periodically DURING a continuous stream of calls — the first call fires immediately, then further calls are ignored until limit ms have passed.
function throttle(fn, limit = 200) {
let waiting = false
return function throttled(...args) {
if (waiting) return
fn.apply(this, args)
waiting = true
setTimeout(() => { waiting = false }, limit)
}
}
// Example usage
const onScroll = throttle(() => console.log('scroll position:', window.scrollY), 200)
window.addEventListener('scroll', onScroll)
Check if a hex color is light or dark
Computes perceived brightness from a hex color to decide whether black or white text reads better on top of it.
How it works: it converts the hex color to RGB and applies the standard luminance-weighted brightness formula (299r + 587g + 114b) — values above 155 are treated as "light" backgrounds.
function isColorLight(hex) {
hex = hex.replace(/^#/, '')
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
}
const r = parseInt(hex.substring(0, 2), 16)
const g = parseInt(hex.substring(2, 4), 16)
const b = parseInt(hex.substring(4, 6), 16)
const brightness = (r * 299 + g * 587 + b * 114) / 1000
return brightness > 155
}
// Example usage — pick readable text color for any background
const textColor = isColorLight('#F5D90A') ? 'black' : 'white'
Check if an event target is an editable field
Guards global keyboard shortcuts from firing while the user is typing in an input, textarea, select, or contenteditable element.
How it works: it checks the element's isContentEditable flag and its tag name against the usual text-entry elements, so you can skip handling a shortcut key while the user is actively typing.
function isEditable(target) {
if (!target) return false
const tag = (target.tagName || '').toLowerCase()
return target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select'
}
// Example usage — only trigger a shortcut when not typing
window.addEventListener('keydown', (e) => {
if (e.key === '/' && !isEditable(document.activeElement)) {
e.preventDefault()
// focus a search input, open a command palette, etc.
}
})
Lazy-Load Autoplaying Videos with IntersectionObserver
Delay video loading until it enters the viewport, then start muted playback once — keeping below-the-fold media out of the initial page load.
How it works: preload="none" asks the browser not to fetch the video eagerly, while omitting autoplay lets the observer control the first playback. Keeping muted and playsinline is important for autoplay policies and inline playback on mobile.
<video
class="lazy"
width="720"
height="404"
loop
muted
playsinline
preload="none"
poster="/images/ocean.png"
src="/videos/demo/ocean.mp4"
></video>
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return
const video = entry.target
video.play().catch(() => {
// Autoplay can still be blocked by browser policy.
})
observer.unobserve(video)
})
})
document.querySelectorAll('.lazy').forEach((video) => {
observer.observe(video)
})
Smooth Scroll to Anchor
Scrolls the page to an element by ID, accounting for an offset (e.g. a fixed header height) — no jQuery, native smooth scroll.
How it works: getBoundingClientRect().top returns the element's position relative to the viewport. Adding window.scrollY converts that to an absolute position, then we subtract the offset for the header. scrollTo({ behavior: 'smooth' }) animates natively.
function scrollToAnchor(id, offsetPx = 0) {
const el = document.getElementById(id)
if (!el) return
const top = el.getBoundingClientRect().top + window.scrollY - offsetPx
window.scrollTo({ top, behavior: 'smooth' })
}
// Version with prefers-reduced-motion support
function scrollToAnchorAccessible(id, offsetPx = 0) {
const el = document.getElementById(id)
if (!el) return
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const top = el.getBoundingClientRect().top + window.scrollY - offsetPx
window.scrollTo({ top, behavior: prefersReduced ? 'instant' : 'smooth' })
}
// Example usage
// Navigation links with a 64px fixed header
document.querySelectorAll('a[href^="#"]').forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault()
const id = link.getAttribute('href').slice(1)
scrollToAnchorAccessible(id, 64)
})
})
sleep() / Async Delay
A minimal sleep function for JavaScript and TypeScript — pauses an async function for a given number of milliseconds without any third-party library.
How it works: returns a Promise that resolves after setTimeout. Awaiting it pauses only the current async function without blocking the event loop.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// Example usage — delay between retry attempts
async function pollUntilReady(checkFn, intervalMs = 500, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
if (await checkFn()) return true
await sleep(intervalMs)
}
return false
}
// Client-side rate limiting between batch requests
for (const batch of batches) {
await processBatch(batch)
await sleep(200)
}
Get IP geolocation info (ipapi.co)
Look up a visitor's country and city from their IP address using the free ipapi.co API.
How it works: a GET request to ipapi.co/{ip}/json/ returns the geolocation data as JSON — including country_name and city. Leaving ip empty makes the API auto-detect the caller's own IP, so it works great for greeting the current visitor.
async function getIpInfo(ip = '') {
const response = await fetch(`https://ipapi.co/${ip}/json/`)
return response.json()
}
// Example usage
const info = await getIpInfo() // empty ip -> detects the visitor's own IP
console.log(`${info.city}, ${info.country_name}`)
const other = await getIpInfo('8.8.8.8') // or look up a specific IP
console.log(other.country_name)
Decode JWT Payload Without Verification
Extracts the payload from a JWT token on the client without verifying the signature — handy for reading exp, userId, and roles in UI. Never use for server-side authorization.
How it works: a JWT has three dot-separated parts. The second part is a Base64URL-encoded JSON payload. This function decodes it and parses the JSON. The signature is not verified — this is for client-side data reading only.
function decodeJwtPayload(token) {
try {
const base64 = token.split('.')[1]
if (!base64) return null
// Base64URL → Base64
const normalized = base64.replace(/-/g, '+').replace(/_/g, '/')
return JSON.parse(atob(normalized))
} catch {
return null
}
}
function isTokenExpired(token) {
const payload = decodeJwtPayload(token)
if (!payload?.exp) return true
return Date.now() >= payload.exp * 1000
}
// Example usage
const payload = decodeJwtPayload(localStorage.getItem('token'))
// { sub: '42', role: 'admin', exp: 1720000000 }
if (isTokenExpired(token)) {
// refresh or log out
}
More snippet collections
Contact Information
Submit a request or write to me directly on Telegram. I'll reply within 24 hours.