Vanilla JavaScript

Modern JavaScript is powerful enough for most web applications without a framework. Here's how to write it well.

DOM Querying

// Single element
const hero = document.querySelector('.hero');

// Multiple elements (returns NodeList — iterable)
const cards = document.querySelectorAll('.card');
cards.forEach(card => card.classList.add('visible'));

// Scoped queries
const sidebar = document.querySelector('.sidebar');
const links = sidebar.querySelectorAll('a');

Event Delegation

Instead of attaching listeners to every element, use delegation:

// BAD: listener per button
document.querySelectorAll('.delete-btn').forEach(btn => {
    btn.addEventListener('click', handleDelete);
});

// GOOD: single listener on parent
document.querySelector('.item-list').addEventListener('click', (e) => {
    const btn = e.target.closest('.delete-btn');
    if (!btn) return;

    const itemId = btn.dataset.id;
    handleDelete(itemId);
});

This handles dynamically added elements automatically.

Fetch API

async function loadPosts(category) {
    const response = await fetch(`/api/posts/?category=${category}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
}

// POST with CSRF (Django)
async function submitForm(data) {
    const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;

    const response = await fetch('/api/submit/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': csrfToken,
        },
        body: JSON.stringify(data),
    });

    return response.json();
}

Custom Elements

Build reusable components with Web Components:

class CopyButton extends HTMLElement {
    connectedCallback() {
        this.innerHTML = `<button class="copy-btn">Copy</button>`;
        this.querySelector('button').addEventListener('click', () => {
            navigator.clipboard.writeText(this.dataset.text);
            this.querySelector('button').textContent = 'Copied!';
            setTimeout(() => {
                this.querySelector('button').textContent = 'Copy';
            }, 2000);
        });
    }
}
customElements.define('copy-button', CopyButton);

Intersection Observer

Lazy-load content or trigger animations on scroll:

const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            entry.target.classList.add('animate-in');
            observer.unobserve(entry.target);
        }
    });
}, { threshold: 0.1 });

document.querySelectorAll('.animate-on-scroll').forEach(el => {
    observer.observe(el);
});