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 APIModern 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

