Vanilla Js In The Real World

The fears around vanilla JS -- routing, state, templates, complexity -- have concrete answers. Here is where the platform is genuinely strong and where lightweight libraries fill the gaps.

May 1, 20264 min read6 / 6

After building a full SPA in vanilla JS -- with routing, web components, a reactive store, and two-way form binding -- it is worth revisiting the objections people raise before writing it off. Some of them have real answers now.

The Essentials

  1. SPA routing is solved: pushState, link interception, and popstate cover the full navigation loop. The server configuration requirement (forwarding all paths to index.html) is the same for React Router and Vue Router.
  2. State management is a design problem, not a platform gap: A Proxy-wrapped store with custom events is a working reactive system. For larger needs, Redux works without React.
  3. Templates are more flexible than they look: Tagged template literals let you build your own rendering logic without JSX or a build step. lit-html is the most mature result of this.
  4. Browser compatibility is no longer a blocker: Custom Elements, Shadow DOM, and Proxy all have full browser support. The only meaningful gap is Declarative Shadow DOM (still arriving in some browsers).
  5. The real cost is people, not code: The bigger constraint is finding developers willing to work without a framework. That has more to do with the current state of education than with the platform itself.

The Fears, Revisited

Before starting this course, the standard concerns about vanilla JS were worth taking seriously. After building with it:

Routing: Not difficult. The History API is four functions. The complexity in framework routers comes from features built on top -- nested routes, route guards, lazy loading -- not from the routing primitive itself.

State management: A Proxy-wrapped store is functional reactive programming without a library. It is more code than useState, but it is also fully transparent. Nothing is happening you cannot explain.

Templating: This is the one area where frameworks genuinely add value if your templates are complex. Tagged template literals give you a path to improve vanilla templates without a framework.

Complexity: Code that looks complex is usually code you do not understand yet. After working through the DOM API, custom events, Proxy, and Shadow DOM, none of it is conceptually harder than what React abstracts over.

Reusable components: Web components can be exported from any framework and used anywhere. A <menu-page> built with custom elements works in React, Angular, Vue, or raw HTML.

Browser compatibility: The only real gap left is Declarative Shadow DOM, which is in the process of arriving everywhere. Everything else -- Proxy, Custom Elements, Shadow DOM, Fetch, History API -- works in every modern browser.

Tagged Template Literals and lit-html

Standard template literals are just string interpolation. Tagged template literals let you process the string with a function:

JavaScript
function html(strings, ...values) { // strings: the static parts of the template // values: the interpolated expressions return strings.reduce((result, str, i) => { return result + (values[i - 1] ?? '') + str; }); } const name = 'Denver'; const markup = html`<h1>Hello, ${name}</h1>`;

The html tag function receives the string segments and values separately. You can do anything here: sanitize values, attach event listeners, track which parts changed and update only those.

lit-html is built on this idea. It intelligently updates only the parts of the DOM that changed between renders. The API looks like this:

JavaScript
import { html, render } from 'lit-html'; const template = (name) => html`<h1>Hello, ${name}</h1>`; render(template('Denver'), document.body);

Using lit-html adds a dependency, but it is small, framework-agnostic, and targets the exact problem of efficient DOM updates from template strings. Adding one focused library like this is not the same as adopting a full framework.

When to Use a Framework

Vanilla JS makes sense when:

  • Performance is a priority and you want to control exactly what ships
  • The project will be maintained for years without framework churn
  • The team understands the platform and the codebase needs to be self-contained
  • You are building web components meant to work across frameworks

A framework makes sense when:

  • The team is larger and you need enforced conventions
  • The app has heavy, complex reactivity (many interdependent state values)
  • Server-side rendering, code splitting, and image optimization are requirements
  • Hiring is a factor and you need a large pool of developers who know the tools

Neither choice is unconditionally right. The point of understanding vanilla JS is that the choice becomes deliberate rather than a default.

Further Reading and Watching

Video:

  • JavaScript Pro Tips by Fireship. A compact tour of modern JavaScript features that complement the patterns in this course.