Frameworks have their place when the product size justifies it, but there’s a huge space where well-organized vanilla JavaScript brings clarity to dependencies and keeps entry costs low for both developers and operations.

The common mistake is to use “vanilla” as a synonym for a giant single file. The solution is to apply a component mindset without forcing a specific runtime.

What I Mean by Component in This Context

A component isn’t necessarily JSX: it’s a self-contained piece with known client-side behavior, accessible markup, and styles with a defined scope. It can expose clear public methods like mount, destroy, or callbacks registered via standard DOM events.

The key is that no one reaches into another component’s DOM internals. If you need communication, use Custom Events or a small in-memory message channel where modules subscribe by topic name, not by imperative references to each other.

Vanilla component system with independent modules and clear boundaries
A component architecture can exist without JSX or a heavy framework.

Folder Structure That Scales Smoothly

A pattern that works very well for content-heavy sites or mid-sized dashboards:

  • components/ with a folder per block (hero/, filter-bar/, …) containing Component.ts, styles.css, and when needed, template.html or helpers.
  • lib/ for shared utilities not tied to UI.
  • pages/ only as compositions that import and mount components.

Stylesheets can be imported from the same module or concatenated at build time if you use Astro, Vite, or similar —the important thing is co-location.

UI contracts between components with well-defined events and boundaries
Clear contracts between components reduce coupling and make technical evolution easier.

Perceived Performance and Bundle

Direct advantage: you only load the JS for the component where the HTML marks it. Compared to a React tree that initializes even unseen views, well-implemented vanilla lets you hydrate with proportional cost.

That helps when your team prioritizes Lighthouse or in markets where speed strongly influences conversion—a topic closely related to other audits I publish on Core Web Vitals.

Migration as the Project Grows

The usual path is to start with well-structured, semantic static markup and add incremental layers:

  1. Local state in each component before exposing shared global state.
  2. A minimal “router” layer that just shows/hides sections or delegates URLs.
  3. Optionally later: adopt tools that compile to the same Web Components model if you want multi-project reuse.

Prioritization criteria for real projects

For vanilla component architecture, the useful question is what decision the team can make more safely after reading the article. Translate the idea into a first workflow, owner, evidence requirement, and measurement plan before committing to implementation.

A practical component contract

A vanilla component should have a contract that is boring enough to be maintained. At minimum, define the selector that activates it, the data attributes it reads, the events it emits, and the lifecycle methods it exposes. That contract lets another developer use the component without opening its internal DOM structure.

export function mountPricingToggle(root: HTMLElement) {
  const buttons = [...root.querySelectorAll<HTMLButtonElement>('[data-plan]')];

  function onClick(event: Event) {
    const target = event.currentTarget as HTMLButtonElement;
    root.dispatchEvent(new CustomEvent('pricing:changed', {
      bubbles: true,
      detail: { plan: target.dataset.plan }
    }));
  }

  buttons.forEach((button) => button.addEventListener('click', onClick));

  return {
    destroy() {
      buttons.forEach((button) => button.removeEventListener('click', onClick));
    }
  };
}

This pattern gives you predictable teardown, testable behavior, and a clear event payload. It also avoids a common anti-pattern: one component reaching into another component’s HTML and changing private implementation details.

When vanilla is enough and when it is not

Vanilla architecture works especially well for marketing sites, documentation sites, server-rendered products, dashboards with isolated widgets, and Astro projects where most pages are content-first. It becomes fragile when the product has deeply shared client-side state, optimistic UI, complex routing, collaborative editing, or many screens whose behavior depends on the same live data model.

SituationVanilla component approachFramework likely justified
Isolated filters, toggles, calculatorsStrong fitUsually unnecessary
Content-heavy site with a few interactive islandsStrong fitOptional
Large app-wide state shared across screensPossible but riskyStronger fit
Reusable design system across many productsWeb Components or frameworkOften justified
Frequent UI experimentation by multiple teamsWorks with disciplineFramework may reduce drift

The decision should be based on the shape of the product, not the popularity of a stack. If the HTML is mostly server-rendered and the interaction is local, a component contract plus Custom Events is often simpler than shipping a full runtime to every visitor.

Testing and maintainability checks

The minimum quality bar is: components can be mounted more than once, destroyed without leaking listeners, operated with keyboard input, and tested with realistic DOM fixtures. For accessibility, the component should not remove native semantics unless it replaces them with correct ARIA behavior and keyboard support.

When a component starts importing too many unrelated helpers, reading global state, or requiring a specific page order, it is no longer isolated. That is the moment to either split the component or introduce a small application layer intentionally instead of letting coupling grow by accident.

Final recommendation

Component-based architecture doesn’t require JSX syntax or a virtual DOM. What it does require are clear contracts, boundaries, and discipline—just like any mature frontend team—with the added benefit that you load less abstraction when your project can still afford it.

Frequently Asked Questions

When does vanilla JavaScript component architecture make sense?
It makes sense when the interaction layer is limited, the team wants low overhead, and components can be expressed with clear contracts, local state, and progressive enhancement.
When should a team move from vanilla components to a framework?
Move when shared state, routing, composition, testing, and team workflow become harder to maintain than the framework cost. The decision should be based on complexity, not preference.
What should a maintainable vanilla component include?
It should define its DOM boundary, inputs, events, lifecycle, styling contract, accessibility rules, and cleanup behavior.

Back to Archive