Vanilla HTML, CSS, and JavaScript: the Best Foundation for Frontend Developers

Introduction

Every few months, someone asks me what framework they should learn to get into frontend development. Angular? React? Vue? And every time, I give the same answer that slightly frustrates them: none of them. Not yet.

Learn HTML. Learn CSS. Learn JavaScript. Properly. Without a framework holding your hand. Then, when you pick up a framework, you’ll understand what it’s actually doing for you — and more importantly, what it’s doing to you.

I started this way, not because I was particularly prescient about it, but because it was the only option available to me at the time. The project was simple, the constraint was real — vanilla HTML, SCSS, and JavaScript, no framework — and I had to make it work. What I didn’t realise until years later was that the constraint was a gift.

After more than a decade of building enterprise systems — I keep returning to the same conviction: the developers who learned the platform before the framework are consistently better at both. And the ones who went straight to the framework are carrying a gap they often don’t know is there.

This is what the foundation gave me, and why I think it should be non-negotiable.

What “Learning Vanilla” Actually Means

Before I get into why it matters, I want to be precise about what I mean — because “learn vanilla JavaScript” can mean anything from a two-hour tutorial to a year of deliberate practice.

I don’t mean memorising every DOM API. I don’t mean being able to implement a virtual DOM from scratch. I mean building things — real, complete things — where the browser is your only tool. A form that validates input before submitting. A page that fetches data from an API and renders it. A layout that responds to different viewport sizes using nothing but CSS. A component-like pattern built from plain JavaScript functions and DOM manipulation.

The point is not the specific output. The point is the constraint. When you have no framework, the browser is fully exposed to you. You have to understand how the event loop works, because nothing is abstracting it. You have to understand the DOM because you’re touching it directly. You have to understand CSS cascade and specificity because there’s no CSS-in-JS scoping your styles. Every problem you solve with vanilla code is a problem you understand at its source.

That understanding is what transfers.

The Browser Doesn’t Care About Your Framework

The first thing the vanilla foundation gave me was a mental model that survived every framework change I went through.

Angular v4 to v18 is a significant migration. The component API changed. The forms module changed. The HTTP client changed. The compiler changed. If your understanding of frontend development is expressed entirely in Angular terms — lifecycle hooks, dependency injection, decorators, zone.js — then a major version upgrade is an identity crisis. The thing you knew is being partially replaced by something different.

But the browser didn’t change. HTML is still a tree of elements. CSS still cascades. JavaScript still runs in a single-threaded event loop. HTTP is still request and response. The DOM is still the DOM.

During the ECAM migration, the developers on the team who had a strong vanilla foundation adapted to each Angular version upgrade with much less friction than those who hadn’t. Not because they knew Angular better — they didn’t, necessarily — but because they understood what Angular was doing underneath. When something broke in the migration, they could reason from first principles: what is the browser doing here, and what is Angular supposed to be doing on top of that? That reasoning path is only available if you’ve been at the level below the framework.

The developers who had learned Angular first, and only Angular, could follow the migration guide and apply the steps. But when the steps didn’t fully account for something in our specific codebase, they were stuck. The migration guide ends where the framework’s documentation ends. First principles don’t end.

CSS Is Not a Solved Problem and Frameworks Won’t Save You

CSS gets dismissed more than any other part of the frontend stack. Developers who are proud of their JavaScript will publicly describe CSS as something they muddle through. The industry has generated an entire category of tooling — CSS-in-JS, utility class frameworks, scoped stylesheets — specifically to make CSS feel more like programming and less like CSS.

And yet, every project I’ve worked on has had significant CSS problems. Not because the tooling failed, but because the developers using the tooling didn’t understand the underlying model.

Specificity conflicts that nobody can resolve because nobody understands how the cascade actually works. Layouts that break on unexpected screen sizes because the developer applied responsive rules by trial and error rather than understanding the box model. Animation jank on mobile because transitions were applied to properties that trigger layout recalculation rather than properties that don’t.

None of these problems are caused by the framework. They’re caused by a gap in CSS understanding that the framework was hired to conceal but couldn’t.

When I built the HPCL mobile web application — no framework, vanilla SCSS, mobile-first — every CSS decision had to be deliberate. There was no utility class to fall back on, no CSS-in-JS to scope away a conflict. I had to understand the cascade to avoid conflicts. I had to understand the box model to build layouts that worked. I had to understand how the browser paints to write animations that didn’t drop frames on a mid-range Android device.

That project was short — three months — but the CSS understanding I was forced to build and apply there informed every subsequent project I worked on, including years of Angular with Tailwind and component-scoped styles. The tools change. The model they’re built on doesn’t.

JavaScript the Language, Not JavaScript the Framework

Angular uses TypeScript. Angular uses RxJS. Angular uses decorators, dependency injection, zone.js, the Ivy compiler. When you write Angular, you are writing a lot of Angular before you are writing JavaScript.

That’s not a criticism. Those abstractions exist for good reasons and they make large-scale application development tractable in ways that plain JavaScript doesn’t. But if your first experience of JavaScript was inside Angular, you are learning the framework and the language simultaneously, and the framework is doing a lot of work to prevent you from needing to understand the language.

The problems with this surface slowly. You reach for an RxJS operator because that’s how data flows in Angular, without understanding what problem it solves or whether it’s the right solution for this case. You write a service with async/await but don’t fully understand the event loop model it’s abstracting. You use Angular’s change detection and get confused when it doesn’t update when you expect, because you don’t have a clear model of JavaScript references and mutation underneath.

I’ve had to debug all of these problems in other people’s code — and, early on, in my own. The fix in each case wasn’t learning more Angular. It was going back to the JavaScript fundamentals and understanding what was actually happening.

The event loop. Closures. Prototype inheritance. The difference between reference and value. How this binding works. Asynchronous execution and the microtask queue. These are not advanced JavaScript topics. They are the language. And a developer who understands them will write better Angular, better React, better anything — because they understand what the framework is managing on their behalf.

The Confidence It Gives You in Debugging

There is a specific kind of debugging panic that I’ve watched junior developers — and some mid-level ones — experience in large enterprise codebases. Something is wrong. The framework is involved. The error message is abstract or framework-specific. And there is no clear path from the error to the cause because the developer’s mental model doesn’t extend below the framework layer.

I have been in that position. It is deeply uncomfortable, and the instinct is to search for an identical Stack Overflow question and apply whatever solution someone else found, without fully understanding why it works.

The vanilla foundation doesn’t eliminate debugging difficulty. Enterprise frontend is genuinely complex and some problems are hard regardless of how well you understand the platform. But it gives you a path when the framework-level reasoning runs out.

When an Angular component isn’t rendering as expected, there is a framework-level answer: is change detection running? Is the input bound correctly? Is the template expression throwing? And there is a platform-level answer: what is in the DOM right now? What events have fired? What has the JavaScript runtime actually executed?

The second path is always available. If you’ve spent time working directly with the DOM — manipulating it, reading from it, responding to its events without a framework mediating — you can always fall back to it. You can always open the browser devtools and reason from what you actually see rather than what the framework model says should be there.

That fallback is worth more than it sounds in the middle of a production debugging session.

How It Made Me a Better Framework Developer

Here is the claim I want to make most directly, because it’s the one I think is most counterintuitive: learning vanilla made me better at Angular, not just better at vanilla.

When I use @Input() and @Output() in an Angular component, I understand that I’m defining a data flow across a DOM boundary — the same boundary that exists in plain HTML between parent and child elements. When I use the Angular HTTP client with RxJS, I understand that underneath the Observable chain, a browser fetch is happening, with all the same behaviour and constraints that fetch always has. When I write SCSS in an Angular component and Angular scopes it with an attribute selector, I understand exactly what’s happening in the CSS cascade and why.

That understanding changes the quality of the code I write. Not because I’m constantly thinking about the underlying layer — mostly I’m not. But because my intuitions about component boundaries, data flow, and side effects are grounded in how the browser actually works, rather than in how the framework represents it.

The developers I’ve mentored who had a strong vanilla foundation picked up Angular faster and more deeply than those who didn’t. Not because they had more experience, but because the framework’s abstractions made immediate sense to them — they could see what the abstraction was abstracting. For a developer who has never worked without the abstraction, the abstraction is just the way things are. When it behaves unexpectedly, there is nowhere lower to look.

The Discipline of Constraints

There is a specific quality that comes from building something real with limited tools, and I don’t think it can be fully replicated by choosing to use vanilla when you have a framework available as an alternative.

The HPCL mobile web application had a hard constraint: vanilla HTML, SCSS, JavaScript, no framework. That constraint forced decisions that an unconstrained project wouldn’t have surfaced. How do you manage component state without a reactive system? How do you handle data fetching and rendering without a declarative template engine? How do you keep your JavaScript organised as it grows without a module system built into the framework?

These are problems that frameworks solve. But solving them yourself, once, teaches you why frameworks solve them the way they do. It gives you an appreciation for what the framework is actually providing — not as an article of faith, but as a lived comparison.

Developers who go straight to a framework often develop a kind of framework dependency that isn’t quite healthy. They trust the framework to handle things they don’t fully understand, and that trust is usually fine until it isn’t — until the edge case the framework doesn’t handle cleanly, until the migration that breaks assumptions they didn’t know they’d made, until the performance problem that requires reasoning below the abstraction layer.

The constraint of vanilla teaches you that you can solve these problems without the framework. That knowledge makes you less dependent on it, more confident in debugging it, and more discerning about when to follow its conventions and when to work around them.

What I Tell Junior Developers Now

When junior developers ask me how to get started in frontend, I give them a project, not a framework.

Build a weather app. No React, no Angular, no Vue. HTML for the structure, CSS for the layout, JavaScript for the API call and DOM update. It doesn’t need to be beautiful. It needs to work, and you need to understand every line of it.

Then build something with more state — a todo list, a simple form with validation, a filterable table. Feel the friction of managing state manually. Notice the patterns that start to emerge in your own code — the way you’re grouping related functions, the way you’re thinking about reusability. That friction and those emerging patterns are exactly what React and Angular are encoding. When you encounter them for the first time in a framework, you’ll recognise them as solutions to problems you’ve already felt.

Then pick up a framework. At that point, it will feel like being given a well-organised toolkit for a workshop you already know how to use. Not like being handed a set of tools and told to build something in a workshop you’ve never entered.

The order matters more than people say it does.

Conclusion

Frameworks are good. I’ve built my career substantially on Angular, and I think it’s an excellent tool for the problems it’s designed to solve. I’m not arguing against frameworks.

I’m arguing for the foundation that makes you genuinely good at them.

A decade of enterprise frontend work has shown me, repeatedly, that the developers who understand the platform — who have built things with just the browser, who have felt the constraints that frameworks exist to remove — are better engineers across the board. They debug faster. They make better architectural decisions. They adapt to framework changes without losing their footing. They know when to follow the framework and when to think past it.

The vanilla foundation is not a prerequisite for getting a job. You can get hired knowing React without knowing what a DOM actually is. The industry lets you do that, and plenty of people do.

But there is a ceiling on what you can understand and a floor on how confident you can be if the browser itself is a black box. I’ve watched developers hit that ceiling and not know why. I’ve watched them debug by copy-pasting Stack Overflow answers they don’t understand. I’ve watched them feel permanently behind — always learning the new framework, never feeling like they actually understand what they’re doing.

The platform doesn’t change the way frameworks do. HTML, CSS, and JavaScript have evolved, but the core model has been stable for decades. Time you invest in understanding it is time that compounds across every framework you ever touch.

Start there. Then go to the framework. You’ll be grateful you did.