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.
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/, …) containingComponent.ts,styles.css, and when needed,template.htmlor 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.
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:
- Local state in each component before exposing shared global state.
- A minimal “router” layer that just shows/hides sections or delegates URLs.
- 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.
| Situation | Vanilla component approach | Framework likely justified |
|---|---|---|
| Isolated filters, toggles, calculators | Strong fit | Usually unnecessary |
| Content-heavy site with a few interactive islands | Strong fit | Optional |
| Large app-wide state shared across screens | Possible but risky | Stronger fit |
| Reusable design system across many products | Web Components or framework | Often justified |
| Frequent UI experimentation by multiple teams | Works with discipline | Framework 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.
Related Reading
- The Real Impact of Performance on Conversion
- Signs Your Web System Needs an Urgent Refactor
- AI Agents for WordPress: Lead Capture and Qualification from the Web
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.