Back to blog

The Return of Vanilla JavaScript in 2026: Less Frameworks, More Clarity

Hello HaWkers, something interesting is happening in the JavaScript ecosystem. After years of "framework wars," a growing movement of developers is consciously choosing to use pure JavaScript in new projects. Not from lack of knowledge, but from clarity of purpose.

Why are experienced developers going back to basics? And when does Vanilla JS make more sense than a framework?

The Vanilla Movement

It's not regression, it's evolution.

Why Now

Factors driving the change:

Mature native APIs:

  • Stable and powerful Fetch API
  • Web Components supported in all browsers
  • Native ES Modules working
  • CSS Container Queries and :has()
  • Dialog, Popover, and other native APIs

Framework fatigue:

  • New framework every week
  • Constant breaking changes
  • Infinite learning curve
  • Outdated dependencies

Performance matters more:

  • Core Web Vitals affect SEO
  • Users on limited devices
  • JavaScript cost too high
  • TTI (Time to Interactive) critical

What Changed in Native JavaScript

The language has evolved significantly.

Modern APIs

What previously needed a library:

// Before: jQuery for AJAX
$.ajax({
  url: '/api/users',
  method: 'GET',
  success: function(data) { /* ... */ }
});

// Now: Native Fetch
const response = await fetch('/api/users');
const users = await response.json();

// Before: Lodash for debounce
import { debounce } from 'lodash';
const debouncedFn = debounce(fn, 300);

// Now: Scheduler API (or simple implementation)
const debouncedFn = (fn, delay) => {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
};

// Before: Moment.js for dates
moment('2026-01-15').format('MM/DD/YYYY');

// Now: Native Intl.DateTimeFormat
new Intl.DateTimeFormat('en-US').format(new Date('2026-01-15'));
// Or soon: Temporal API

Modern DOM Manipulation

Without jQuery, with power:

// Element selection
const button = document.querySelector('.submit-btn');
const items = document.querySelectorAll('.item');

// Efficient event delegation
document.querySelector('.list').addEventListener('click', (e) => {
  if (e.target.matches('.item')) {
    handleItemClick(e.target);
  }
});

// Class manipulation
element.classList.add('active', 'visible');
element.classList.toggle('open');
element.classList.replace('old', 'new');

// Data attributes
element.dataset.userId = '123';
const id = element.dataset.userId;

// Template literals for HTML
const html = `
  <div class="card">
    <h2>${title}</h2>
    <p>${description}</p>
  </div>
`;
container.insertAdjacentHTML('beforeend', html);

// Intersection Observer (native lazy loading)
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadImage(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

Native Web Components

Components without framework.

Custom Elements

Creating reusable components:

// Component definition
class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  static get observedAttributes() {
    return ['name', 'avatar', 'role'];
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  render() {
    const name = this.getAttribute('name') || 'Unknown';
    const avatar = this.getAttribute('avatar') || '/default.png';
    const role = this.getAttribute('role') || 'User';

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 1rem;
          border-radius: 8px;
          background: var(--card-bg, #fff);
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .card {
          display: flex;
          align-items: center;
          gap: 1rem;
        }
        img {
          width: 48px;
          height: 48px;
          border-radius: 50%;
        }
        h3 { margin: 0; }
        span { color: #666; font-size: 0.9rem; }
      </style>
      <div class="card">
        <img src="${avatar}" alt="${name}">
        <div>
          <h3>${name}</h3>
          <span>${role}</span>
        </div>
      </div>
    `;
  }
}

customElements.define('user-card', UserCard);
<!-- Usage in HTML -->
<user-card
  name="John Smith"
  avatar="/avatars/john.jpg"
  role="Senior Developer">
</user-card>

Slots and Composition

Flexible components:

class Modal extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: none;
          position: fixed;
          inset: 0;
          background: rgba(0,0,0,0.5);
          place-items: center;
        }
        :host([open]) {
          display: grid;
        }
        .modal {
          background: white;
          padding: 2rem;
          border-radius: 8px;
          max-width: 500px;
          width: 90%;
        }
        ::slotted([slot="header"]) {
          margin-top: 0;
        }
      </style>
      <div class="modal">
        <slot name="header"></slot>
        <slot></slot>
        <slot name="footer"></slot>
      </div>
    `;

    // Close on click outside
    this.addEventListener('click', (e) => {
      if (e.target === this) {
        this.close();
      }
    });
  }

  open() {
    this.setAttribute('open', '');
  }

  close() {
    this.removeAttribute('open');
    this.dispatchEvent(new CustomEvent('modal-close'));
  }
}

customElements.define('app-modal', Modal);
<app-modal id="confirmModal">
  <h2 slot="header">Confirm Action</h2>
  <p>Are you sure you want to continue?</p>
  <div slot="footer">
    <button onclick="confirmModal.close()">Cancel</button>
    <button onclick="confirm()">Confirm</button>
  </div>
</app-modal>

State Without Framework

Managing state with pure JavaScript.

Simple Store

Minimalist pub/sub pattern:

// store.js - Simple global state
function createStore(initialState = {}) {
  let state = initialState;
  const listeners = new Set();

  return {
    getState() {
      return state;
    },

    setState(newState) {
      state = typeof newState === 'function'
        ? newState(state)
        : { ...state, ...newState };

      listeners.forEach(listener => listener(state));
    },

    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    }
  };
}

// Usage
const store = createStore({
  user: null,
  items: [],
  loading: false
});

// Component subscribes
const unsubscribe = store.subscribe((state) => {
  renderUserInfo(state.user);
  renderItems(state.items);
});

// Update state
store.setState({ loading: true });
const items = await fetchItems();
store.setState({ items, loading: false });

Reactive Proxy

Reactivity without library:

// Proxy for reactive state
function reactive(target, onChange) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const oldValue = obj[prop];
      obj[prop] = value;

      if (oldValue !== value) {
        onChange(prop, value, oldValue);
      }

      return true;
    },

    get(obj, prop) {
      const value = obj[prop];
      if (typeof value === 'object' && value !== null) {
        return reactive(value, onChange);
      }
      return value;
    }
  });
}

// Usage
const state = reactive({
  count: 0,
  user: { name: 'John' }
}, (prop, newValue, oldValue) => {
  console.log(`${prop} changed: ${oldValue}${newValue}`);
  updateUI();
});

state.count++; // Triggers onChange
state.user.name = 'Mary'; // Also works on nested objects

When to Use Vanilla JS

Choosing consciously.

Ideal Cases

Where Vanilla JS shines:

Landing pages:

  • Limited interactivity
  • Critical performance
  • SEO important
  • Mostly static content

Embeddable widgets:

  • No conflict with host
  • Minimal bundle
  • Total independence

Libraries and tools:

  • Without assuming user's framework
  • Maximum compatibility
  • Smallest footprint

Content sites:

  • Blogs, documentation
  • Portfolios
  • Institutional sites

When Framework Still Makes Sense

It's not all or nothing:

Complex SPAs:

  • Many interdependent states
  • Extensive client-side routing
  • Large team working together

Real-time applications:

  • Dashboards with many updates
  • Chat and collaboration
  • Highly dynamic interfaces

Team productivity:

  • Team already knows the framework
  • Mature ecosystem needed
  • Established patterns

Tools for Vanilla JS

Modern development without framework.

Build Tools

Minimal configuration:

// vite.config.js for vanilla project
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: 'index.html',
        about: 'about.html'
      }
    }
  }
});

TypeScript Works

Typing without framework:

// types.ts
interface User {
  id: string;
  name: string;
  email: string;
}

interface AppState {
  user: User | null;
  items: Item[];
  loading: boolean;
}

// store.ts
function createStore<T>(initialState: T) {
  let state = initialState;
  const listeners = new Set<(state: T) => void>();

  return {
    getState(): T {
      return state;
    },

    setState(newState: Partial<T> | ((prev: T) => T)): void {
      state = typeof newState === 'function'
        ? newState(state)
        : { ...state, ...newState };

      listeners.forEach(listener => listener(state));
    },

    subscribe(listener: (state: T) => void): () => void {
      listeners.add(listener);
      return () => listeners.delete(listener);
    }
  };
}

const store = createStore<AppState>({
  user: null,
  items: [],
  loading: false
});

Bundle Size Comparison

Numbers that matter:

Approach Bundle Size First Paint
Vanilla JS ~5 KB ~50ms
Preact ~10 KB ~80ms
Vue 3 ~35 KB ~120ms
React ~45 KB ~150ms
Angular ~100 KB ~250ms

💡 Note: These numbers are approximate and vary by application. The point is that Vanilla JS has zero framework overhead.

The return of Vanilla JavaScript isn't about rejecting frameworks - it's about choosing the right tool for each job. With mature native APIs, stable Web Components, and a more powerful language than ever, pure JavaScript is a legitimate and often superior choice.

If you want to master the fundamentals that never change, I recommend checking out another article: ES2026 and Temporal API where you'll discover the new native language features.

Let's go! 🦅

💻 Master JavaScript for Real

The knowledge you gained in this article is just the beginning. Mastering pure JavaScript is the foundation every developer needs.

Invest in Your Future

I've prepared complete material for you to master JavaScript:

Payment options:

  • 1x of $4.90 no interest
  • or $4.90 at sight

📖 View Complete Content

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments