What Is Vanilla JavaScript? A Practical Guide (2026)

You open a small web project to fix a tiny UI bug. The node_modules folder is 900 MB, the build takes longer than the change, and you’re stuck waiting for a toolchain you didn’t even mean to touch. I’ve been there. It’s also why I still care about Vanilla JavaScript in 2026.

Vanilla JavaScript means writing plain JavaScript without bringing in a framework or library as a dependency for the core behavior. You’re working with the language itself and the browser’s built-in APIs: the DOM, events, fetch, modules, storage, and more. Sometimes that’s the whole project. Other times it’s the glue that holds a larger app together.

If you build for the web, you should understand the vanilla layer even if you ship React, Vue, or Angular daily. It’s the difference between guessing and knowing when something breaks in production, between copying patterns and being able to design them. I’m going to show you what Vanilla JavaScript is, why the term exists, what modern vanilla looks like (not 2012-era scripts), and how to decide when vanilla is the right call.

What ‘vanilla’ means (and what it doesn’t)

Vanilla JavaScript is just JavaScript as the platform ships it: no jQuery, no React, no state management package, no DOM helper library. That’s it.

When people say ‘vanilla’, they usually mean a few specific things:

  • You call browser APIs directly (document.querySelector, addEventListener, fetch, history.pushState, localStorage).
  • You ship less code to the browser because you’re not bundling a library for basic tasks.
  • You own the architecture. That’s a gift for small apps and a responsibility for large ones.

What vanilla does NOT mean:

  • No tooling. In 2026, you can write vanilla code and still use a bundler, TypeScript, ESLint, tests, and a dev server. Tooling is not the same thing as a runtime dependency.
  • ‘Old-school’ JavaScript. Modern vanilla uses ES modules, async/await, AbortController, Intl, IntersectionObserver, requestAnimationFrame, and web platform primitives that didn’t exist (or weren’t usable) years ago.
  • Avoiding frameworks as a belief system. I like frameworks. I also like not reaching for them when they’re unnecessary.

A quick way I keep it straight is this:

  • Vanilla is about what you ship and rely on at runtime.
  • Tooling is about how you build, test, and ship.

If the browser can run your app without downloading a UI framework, you’re in vanilla territory—even if you used great tools to create it.

Why it’s called ‘vanilla’

The term is a playful metaphor: vanilla ice cream is the plain baseline flavor. It’s not ‘worse’; it’s the default you build from.

That metaphor fits software surprisingly well. Vanilla JavaScript is the baseline experience:

  • Minimal moving parts
  • Minimal dependency risk
  • Direct understanding of what the browser is doing

If you’ve ever debugged a production issue where a third-party dependency changed behavior after an update, you already understand the emotional appeal of ‘vanilla’. No mystery helpers, no hidden abstractions.

Here’s a tiny vanilla snippet that you can run as-is in the browser console or Node:

console.log(‘Hello from Vanilla JavaScript!‘);

// Simple function to add two numbers

function add(a, b) {

return a + b;

}

const result = add(5, 7);

console.log(‘The sum of 5 and 7 is:‘, result);

That example is intentionally plain: just language features and the runtime.

Vanilla JavaScript in 2026: the platform is the toolbox

A lot of older advice about needing a library for ‘everything’ came from a time when browsers were inconsistent and missing key features. In 2026, the platform is far more capable. When I write vanilla today, I’m usually relying on:

DOM selection and manipulation

You don’t need helper wrappers for most tasks:

  • document.querySelector() / querySelectorAll()
  • element.classList (add/remove/toggle)
  • element.dataset for data attributes
  • element.setAttribute() and element.textContent

What’s changed is not just capability—it’s confidence. If you know the DOM API well, you stop writing defensive wrapper code and start writing intent.

A practical example: if you catch yourself doing repeated queries inside a loop, it’s usually cheaper (and clearer) to cache references once.

const list = document.querySelector(‘#items‘);

const rows = list.querySelectorAll(‘li‘);

Events and user interaction

The browser’s event system is strong when you use it well:

  • Event delegation (one handler on a parent)
  • passive: true where appropriate for scroll/touch
  • pointerdown/pointerup for unified input (mouse/stylus/touch)

In vanilla, events are where architecture starts to matter. If you attach 50 listeners to 50 buttons, it works—until you add dynamic rendering and forget to reattach or clean up. Delegation avoids a whole category of bugs.

Networking

The fetch ecosystem matured:

  • fetch() with async/await
  • AbortController for canceling requests
  • Streams where needed (large downloads, incremental parsing)

Where people get into trouble is not the happy path—it’s timeouts, retries, cancelation, and race conditions (like a slow response arriving after the user already navigated away). Vanilla gives you the primitives; you decide the rules.

Modules

ES modules changed how vanilla code is structured:

  • type=‘module‘ in the browser
  • import/export for composable code
  • Clear dependency graph without a framework

Modules are the biggest reason modern vanilla doesn’t have to look like ‘a pile of scripts’. If you can break a feature into api, state, and ui, your app gets easier to change.

State and persistence

For many apps, built-ins are enough:

  • localStorage for small key-value persistence
  • IndexedDB for larger client-side data
  • URLSearchParams for shareable state in URLs

One rule I follow: localStorage is a great cache and preference store, not a database. If you need querying, transactions, or large blobs, that’s where IndexedDB earns its keep.

Performance primitives

You can keep interfaces snappy with:

  • requestAnimationFrame for animation-related updates
  • IntersectionObserver for lazy loading and visibility tracking
  • ResizeObserver for layout-driven UI

When you know these primitives, you can build a lot without dragging a framework into the runtime.

A practical vanilla example: a mini task list with real patterns

I want something more realistic than printing to the console, but still small enough to understand quickly. This example:

  • Renders a task list
  • Uses event delegation
  • Persists to localStorage
  • Avoids re-rendering everything unnecessarily

Create an index.html and paste this in. Open it in a browser.

Vanilla Tasks

body { font-family: system-ui, -apple-system, Segoe UI, sans-serif; margin: 2rem; }

.row { display: flex; gap: 0.5rem; }

input[type=‘text‘] { flex: 1; padding: 0.5rem; }

button { padding: 0.5rem 0.75rem; }

ul { padding-left: 1.25rem; }

li { margin: 0.5rem 0; }

.done { text-decoration: line-through; opacity: 0.65; }

Tasks

    const STORAGEKEY = ‘vanillatasks_v1‘;

    / @type {{ id: string, title: string, done: boolean, createdAt: number }[]} */

    let tasks = loadTasks();

    const taskForm = document.querySelector(‘#taskForm‘);

    const taskInput = document.querySelector(‘#taskInput‘);

    const taskList = document.querySelector(‘#taskList‘);

    const meta = document.querySelector(‘#meta‘);

    renderAll();

    taskForm.addEventListener(‘submit‘, (e) => {

    e.preventDefault();

    const title = taskInput.value.trim();

    if (!title) return;

    const task = {

    id: crypto.randomUUID(),

    title,

    done: false,

    createdAt: Date.now(),

    };

    tasks = [task, …tasks];

    saveTasks(tasks);

    taskInput.value = ‘‘;

    prependTask(task);

    renderMeta();

    });

    // Event delegation: one listener handles clicks for all list items.

    taskList.addEventListener(‘click‘, (e) => {

    const btn = e.target.closest(‘button[data-action]‘);

    if (!btn) return;

    const li = e.target.closest(‘li[data-id]‘);

    if (!li) return;

    const id = li.dataset.id;

    const action = btn.dataset.action;

    if (action === ‘toggle‘) {

    tasks = tasks.map((t) => (t.id === id ? { …t, done: !t.done } : t));

    saveTasks(tasks);

    syncTaskRow(li, tasks.find((t) => t.id === id));

    renderMeta();

    return;

    }

    if (action === ‘delete‘) {

    tasks = tasks.filter((t) => t.id !== id);

    saveTasks(tasks);

    li.remove();

    renderMeta();

    return;

    }

    });

    function renderAll() {

    taskList.innerHTML = ‘‘;

    for (const task of tasks) {

    taskList.appendChild(createTaskRow(task));

    }

    renderMeta();

    }

    function renderMeta() {

    const total = tasks.length;

    const done = tasks.filter((t) => t.done).length;

    meta.textContent = ${done} done / ${total} total;

    }

    function createTaskRow(task) {

    const li = document.createElement(‘li‘);

    li.dataset.id = task.id;

    const title = document.createElement(‘span‘);

    title.textContent = task.title;

    if (task.done) title.classList.add(‘done‘);

    const toggle = document.createElement(‘button‘);

    toggle.type = ‘button‘;

    toggle.dataset.action = ‘toggle‘;

    toggle.textContent = task.done ? ‘Undo‘ : ‘Done‘;

    const del = document.createElement(‘button‘);

    del.type = ‘button‘;

    del.dataset.action = ‘delete‘;

    del.textContent = ‘Delete‘;

    li.append(title, document.createTextNode(‘ ‘), toggle, document.createTextNode(‘ ‘), del);

    return li;

    }

    function syncTaskRow(li, task) {

    if (!task) return;

    const title = li.querySelector(‘span‘);

    const toggle = li.querySelector("button[data-action=‘toggle‘]");

    title.textContent = task.title;

    title.classList.toggle(‘done‘, task.done);

    toggle.textContent = task.done ? ‘Undo‘ : ‘Done‘;

    }

    function prependTask(task) {

    taskList.prepend(createTaskRow(task));

    }

    function loadTasks() {

    try {

    const raw = localStorage.getItem(STORAGE_KEY);

    if (!raw) return [];

    const data = JSON.parse(raw);

    if (!Array.isArray(data)) return [];

    // Basic validation: keep only well-formed items.

    return data

    .filter((t) => t && typeof t.id === ‘string‘ && typeof t.title === ‘string‘)

    .map((t) => ({

    id: t.id,

    title: t.title,

    done: Boolean(t.done),

    createdAt: typeof t.createdAt === ‘number‘ ? t.createdAt : Date.now(),

    }));

    } catch {

    return [];

    }

    }

    function saveTasks(next) {

    localStorage.setItem(STORAGE_KEY, JSON.stringify(next));

    }

    Why I like this example for learning vanilla:

    • You see the real DOM API without wrappers.
    • You see one of the most important scaling tricks: event delegation.
    • You keep state in plain objects and arrays.
    • You explicitly decide when to re-render.

    That’s the essence of vanilla: explicit control.

    Key characteristics (the real trade-offs)

    When I describe Vanilla JavaScript to a team, I keep it practical. Here are the characteristics that matter in real projects.

    No runtime dependencies

    You’re not shipping a library just to handle core behavior. The upside is smaller payloads and fewer supply-chain updates to babysit. The downside is you write more of your own structure.

    A nuance I want to highlight: you can still depend on tiny utilities if they’re truly worth it, but the spirit of vanilla is avoiding heavy runtime dependencies for problems the platform already solves well.

    Direct browser API usage

    Vanilla code interacts with the platform the way it is. That makes debugging clearer because stack traces map to your code and native functions, not to a thick abstraction layer.

    It also means you need to learn the real edges:

    • Some APIs are synchronous and can block (for example, localStorage writes on the main thread).
    • Some APIs are powerful but easy to misuse (for example, innerHTML can introduce security issues).

    Lightweight by default

    For small apps, vanilla can feel immediate: load the HTML, attach listeners, render. Performance-wise, you often save startup time because you skip framework boot and additional parsing work. That’s not a promise; it’s just what tends to happen when you ship less JavaScript.

    But lightweight doesn’t mean ‘fast no matter what’. If you do a thousand DOM writes in a loop, you can make vanilla crawl. The platform is fast when you cooperate with it.

    Foundational knowledge

    Frameworks are easier when you understand what they’re wrapping:

    • Virtual DOM makes more sense when you know DOM costs.
    • Router behavior makes more sense when you know History API.
    • State management makes more sense when you’ve felt the pain of tangled state.

    This is the underrated value of vanilla: even if you never ship a production vanilla app, learning it upgrades how you use every framework.

    Vanilla vs framework in 2026: a decision table I actually use

    I don’t choose vanilla because it’s ‘pure’. I choose it when it reduces risk and total work.

    Here’s a practical comparison.

    Scenario

    Vanilla JavaScript tends to win

    A framework tends to win —

    — Marketing page with light interactivity

    Minimal JS payload, fast shipping

    Often unnecessary overhead Small internal tool (1-3 screens)

    Quick build, easy deployment

    Can be fine, but adds build complexity Large app (many routes, deep state)

    You must design architecture carefully

    Built-in patterns help teams scale UI needs are mostly form + CRUD

    Simple DOM + fetch works well

    Framework form libraries can help Highly interactive UI (drag/drop, complex state)

    Possible, but you’ll write more code

    Component model pays off Long-lived product with many contributors

    Requires strong conventions

    Conventions often come built-in Embeddable widget for third-party sites

    No dependency conflicts, smaller bundle

    Framework may conflict with host page

    My specific recommendation:

    • If the project is a widget, a documentation site, a small dashboard, or a single page with a couple of interactive components, start vanilla.
    • If the project is a product UI with dozens of components, heavy shared state, and many engineers touching it weekly, pick a framework early.

    That’s not hedging. It’s how you avoid spending six months rebuilding your own half-framework.

    What vanilla looks like when it scales (without turning into chaos)

    The biggest knock on vanilla is maintainability. That critique is fair when vanilla means ‘a pile of scripts’. Modern vanilla doesn’t have to be that.

    Here are patterns I rely on.

    1) Modules and boundaries

    Even without a framework, I structure code like this:

    • api/ for network calls
    • state/ for state and persistence
    • ui/ for DOM rendering and event binding
    • utils/ for pure helpers

    If you can keep most logic pure (no DOM, no globals), you can test it easily and reuse it later.

    A concrete boundary rule I like: UI modules are allowed to touch the DOM; state modules are not. That single decision prevents a lot of spaghetti.

    2) Event delegation as the default

    Instead of attaching listeners to every button, attach one listener to the list or container. This reduces memory overhead and prevents bugs when items are added/removed.

    One more trick: delegate by intent, not by tag.

    container.addEventListener(‘click‘, (e) => {

    const el = e.target.closest(‘[data-action]‘);

    if (!el || !container.contains(el)) return;

    switch (el.dataset.action) {

    case ‘save‘:

    // …

    break;

    case ‘cancel‘:

    // …

    break;

    }

    });

    When you standardize on data-action, new UI becomes easy: add a button, set an action, handle it in one place.

    3) One-way data flow (yes, even in vanilla)

    I like a simple mental model:

    • State changes happen in one place.
    • Rendering reads state and updates the DOM.
    • Events dispatch intent (toggle, delete, save).

    You don’t need a library to practice disciplined state flow.

    If you want a lightweight pattern, I’ve had success with a tiny store:

    function createStore(initial) {

    let state = initial;

    const listeners = new Set();

    return {

    get: () => state,

    set: (updater) => {

    state = typeof updater === ‘function‘ ? updater(state) : updater;

    for (const fn of listeners) fn(state);

    },

    subscribe: (fn) => {

    listeners.add(fn);

    return () => listeners.delete(fn);

    },

    };

    }

    That’s not a framework. It’s just enough structure to keep updates consistent.

    4) Progressive enhancement

    If the page can mostly work without JavaScript, you get resilience:

    • HTML forms that submit normally, upgraded to fetch when JS is available
    • Links that work normally, upgraded to in-page navigation when JS is available

    This matters for reliability and accessibility.

    A simple progressive-enhancement pattern for forms:


    const form = document.querySelector(‘#profileForm‘);

    form.addEventListener(‘submit‘, async (e) => {

    e.preventDefault();

    const fd = new FormData(form);

    const res = await fetch(form.action, { method: ‘POST‘, body: fd });

    if (!res.ok) {

    // show an error

    return;

    }

    // show success

    });

    If JavaScript fails, the form still works. If JavaScript succeeds, the UX improves.

    5) Web Components when you need encapsulation

    When I need reusable UI pieces without bringing a framework, I reach for Custom Elements + Shadow DOM. Not every team loves them, but for isolated widgets (especially embedded ones), they’re a strong vanilla-native option.

    Here’s a tiny, runnable Custom Element example that’s genuinely useful: a copy-to-clipboard button that updates its label and stays accessible.

    class CopyButton extends HTMLElement {

    static observedAttributes = [‘value‘];

    constructor() {

    super();

    this.attachShadow({ mode: ‘open‘ });

    this._value = this.getAttribute(‘value‘) ?? ‘‘;

    this._timeoutId = null;

    this.shadowRoot.innerHTML = `

    button {

    font: inherit;

    padding: 0.5rem 0.75rem;

    border-radius: 0.5rem;

    border: 1px solid #ccc;

    background: #fff;

    cursor: pointer;

    }

    button:focus-visible {

    outline: 2px solid #4c9ffe;

    outline-offset: 2px;

    }

    `;

    this._btn = this.shadowRoot.querySelector(‘button‘);

    this.onClick = this.onClick.bind(this);

    }

    connectedCallback() {

    this.btn.addEventListener(‘click‘, this.onClick);

    }

    disconnectedCallback() {

    this.btn.removeEventListener(‘click‘, this.onClick);

    if (this.timeoutId) clearTimeout(this.timeoutId);

    }

    attributeChangedCallback(name, _oldValue, newValue) {

    if (name === ‘value‘) this._value = newValue ?? ‘‘;

    }

    async _onClick() {

    try {

    await navigator.clipboard.writeText(this._value);

    this._flash(‘Copied‘);

    } catch {

    this._flash(‘Copy failed‘);

    }

    }

    _flash(label) {

    this._btn.textContent = label;

    if (this.timeoutId) clearTimeout(this.timeoutId);

    this._timeoutId = setTimeout(() => {

    this._btn.textContent = ‘Copy‘;

    }, 1200);

    }

    }

    customElements.define(‘copy-button‘, CopyButton);

    That’s vanilla, but it has real-world traits: lifecycle cleanup, attribute updates, and a UX that doesn’t require any surrounding framework.

    Common pitfalls (and how I avoid them)

    Vanilla code fails for predictable reasons. If you know the failure modes, you can avoid them early.

    1) Too many direct DOM writes

    The DOM is fast, but layout and style recalculation are not free. The easiest foot-gun is mixing reads and writes in a way that forces repeated layout.

    What I try to do instead:

    • Batch DOM updates (build a DocumentFragment, then append once).
    • Prefer toggling classes over setting many inline styles.
    • Avoid reading layout (getBoundingClientRect) inside loops that also write.

    Example: create list rows in memory first.

    const frag = document.createDocumentFragment();

    for (const item of items) frag.appendChild(renderRow(item));

    list.replaceChildren(frag);

    2) Leaky event listeners

    In a small script, leaking a listener doesn’t show up. In a long-lived app (or a widget embedded on many pages), it becomes a real problem.

    My rule: if you add a listener in a setup function, return a cleanup function.

    function mount(el) {

    const onClick = () => {};

    el.addEventListener(‘click‘, onClick);

    return () => el.removeEventListener(‘click‘, onClick);

    }

    That single habit scales surprisingly well.

    3) Races in async code

    A classic bug: the user types quickly, you fire multiple requests, and a slower response arrives last and overwrites the newest result.

    Fix: cancel the previous request with AbortController.

    let controller = null;

    async function search(q) {

    if (controller) controller.abort();

    controller = new AbortController();

    const res = await fetch(/api/search?q=${encodeURIComponent(q)}, {

    signal: controller.signal,

    });

    if (!res.ok) return;

    const data = await res.json();

    renderResults(data);

    }

    Even if you don’t cancel, you should at least guard against out-of-order responses.

    4) Overusing innerHTML

    innerHTML is convenient and sometimes appropriate, but it’s also the easiest way to create XSS vulnerabilities if any part of the string comes from user input or an untrusted source.

    My default:

    • Use textContent for text.
    • Use document.createElement for structure.
    • If I must use HTML strings, I treat the source as untrusted unless I can prove otherwise.

    5) Reinventing a framework accidentally

    This is the sneaky one: you start vanilla, then add your own templating, your own router, your own state system, your own component lifecycle… and six months later you have a half-framework with none of the battle testing.

    My check:

    • If I’m building shared abstractions for the third time, I either standardize them hard or I adopt a framework.

    Performance considerations (measuring, not guessing)

    Vanilla is often fast because it can be small. But performance is not a vibe—it’s a set of constraints.

    Here’s how I think about it in real projects.

    Startup cost: parse, compile, execute

    Shipping less JavaScript generally reduces startup work. Vanilla helps because you don’t have to ship a runtime framework for simple interaction.

    But you can still shoot yourself in the foot by:

    • Doing heavy work at module top-level (runs immediately on import).
    • Running expensive loops before the first paint.

    A simple trick: defer non-critical work until the browser is idle.

    function scheduleLowPriority(fn) {

    if (‘requestIdleCallback‘ in window) {

    requestIdleCallback(() => fn(), { timeout: 1000 });

    } else {

    setTimeout(fn, 0);

    }

    }

    DOM cost: fewer updates, smarter updates

    You don’t need a virtual DOM to be efficient; you need to update the DOM with intent.

    Vanilla patterns I rely on:

    • Update a single row instead of re-rendering the whole list (like the task list example).
    • Use CSS for visual states; toggle a class.
    • Use content-visibility: auto for large, offscreen sections when appropriate.

    Images and network

    Vanilla doesn’t automatically solve your largest payloads (images, fonts, third-party scripts). I still treat these as part of ‘vanilla performance’ because they’re part of what the browser loads.

    A couple of easy wins:

    • Lazy-load images (loading=‘lazy‘) where it makes sense.
    • Avoid shipping multiple font files if you can.
    • Keep third-party tags on a tight leash.

    Observability: keep it simple

    You can do lightweight performance checks right in DevTools:

    • Performance panel for long tasks and layout thrash
    • Network panel for payload sizes
    • Lighthouse for broad signals

    The vanilla mindset I aim for is: make the fast path obvious, then confirm with measurement.

    Debugging and maintainability: my vanilla workflow

    One reason I still like vanilla is how directly it maps to what the browser is doing.

    I debug from the edges inward

    When something breaks, I ask:

    • Did the event fire?
    • Did the handler run?
    • Did the state change?
    • Did the DOM update?

    This is basically a manual version of what framework devtools show you. Vanilla forces you to build the mental model yourself, and that mental model transfers to every stack.

    I log less, inspect more

    A few tools I lean on:

    • Breakpoints in event listeners
    • DOM breakpoints on subtree modifications
    • console.table for state arrays
    • debugger as a deliberate temporary stop, not a habit

    I design for stack traces

    I prefer named functions over anonymous ones in complex flows because stack traces become readable.

    function onTaskListClick(e) {

    // …

    }

    taskList.addEventListener(‘click‘, onTaskListClick);

    That’s not about style points. It’s about faster production debugging.

    Tooling you can use without abandoning vanilla

    Vanilla is not anti-tooling. In fact, a small amount of tooling can make vanilla dramatically more maintainable.

    Here’s what I consider ‘vanilla-friendly’ tooling:

    • Type checking (TypeScript or // @ts-check with JSDoc)
    • Linting/formatting (ESLint + Prettier, or whatever your team uses)
    • A dev server with HMR (nice-to-have, not required)
    • Unit tests for pure logic (especially state and parsing)

    A practical middle ground I like for small projects: keep runtime code vanilla JS, but enable editor type checking with JSDoc.

    // @ts-check

    / @param {string} raw */

    export function parseCount(raw) {

    const n = Number(raw);

    return Number.isFinite(n) ? n : 0;

    }

    That gives you guardrails without forcing a full compile step.

    Alternative approaches inside the vanilla universe

    Vanilla isn’t one style. There are a few ‘sub-styles’ that are still vanilla at runtime.

    1) Minimal functional core, imperative shell

    This is my default:

    • Keep data transformations pure.
    • Keep DOM and fetch at the edges.

    It makes testing easier and side effects more obvious.

    2) Web Components as the component model

    If your pain is component reuse and style isolation, Web Components can be the vanilla way to get it.

    Where I think they shine:

    • Embeddable widgets
    • Design-system primitives
    • Isolated interactive components

    Where I’m cautious:

    • Very large apps with many interdependent components (you may end up wanting a framework’s ecosystem)
    • Teams unfamiliar with Shadow DOM quirks (styling and theming needs planning)

    3) HTML-first with small islands of JS

    This is a strategy, not a library:

    • Render most content as HTML.
    • Add JS only where it improves UX.

    It’s great for content-heavy pages, docs, and marketing flows where the baseline must remain robust.

    When NOT to use vanilla (the honest list)

    I like vanilla, but I don’t romanticize it. I usually avoid pure vanilla for:

    Highly complex state with many contributors

    If your app has deep shared state, many routes, lots of reuse, and multiple teams, a mature framework can reduce total coordination cost.

    Extremely dynamic UI with lots of component composition

    You can build it in vanilla, but you’ll spend time rebuilding patterns frameworks already solved:

    • Component lifecycle
    • Derived state and memoization
    • Rendering optimizations
    • Developer ergonomics

    Projects that depend heavily on an ecosystem

    Sometimes the decision is made for you by requirements:

    • A specific set of UI components
    • A mature form solution
    • Established patterns your org already uses

    In those cases, vanilla knowledge still helps, but vanilla might not be the best delivery choice.

    A short learning checklist (what I’d focus on)

    If you’re learning vanilla intentionally, here’s the order I’d pick for maximum practical payoff:

    1) DOM basics: querySelector, classList, creating elements, textContent

    2) Events: bubbling, delegation, preventDefault, keyboard events

    3) Async: fetch, error handling, AbortController

    4) Modules: import/export, separating api vs ui vs state

    5) Browser APIs that feel like superpowers: IntersectionObserver, ResizeObserver, Intl, URL/URLSearchParams

    6) Basics of performance: batching DOM updates, avoiding layout thrash

    7) Basics of security: avoid unsafe HTML injection, encode URL params, handle untrusted data

    If you can do those, you’re not just ‘using vanilla’. You’re using the web platform.

    Final takeaway

    Vanilla JavaScript is not a trend or a nostalgia trip. It’s the foundation of everything you do on the web: the language and the platform, without a runtime framework sitting in between.

    Sometimes vanilla is the whole app. Sometimes it’s the thin layer inside a bigger stack where you need direct control: a widget, a form enhancement, a performance fix, a tricky event flow, a tiny feature that shouldn’t drag in a new dependency.

    And even when you choose a framework (often the right call), knowing vanilla is what keeps you from being trapped by it. It’s how you debug confidently, evaluate trade-offs clearly, and build web UIs that feel simple because they’re built on solid fundamentals.

    Scroll to Top