In the beginning, there was only HTML. The first official HTML specification focused on semantic markup. There were minimal styling tags and attributes. It was up to the web browser how to render the markup in an HTML document.
The whole specification was refreshingly simple. You could easily read it in one sitting. Over three decades later, we are still using the same HTML tags and then some.
In Principles of Design, Tim Berners-Lee, explains why he came up with HTML as the language to define web pages:
Computer Science in the 1960s to 80s spent a lot of effort making languages that were as powerful as possible. Nowadays we have to appreciate the reasons for picking not the most powerful solution but the least powerful. The reason for this is that the less powerful the language, the more you can do with the data stored in that language. If you write it in a simple declarative form, anyone can write a program to analyze it in many ways. (…) I chose HTML not to be a programming language because I wanted different programs to do different things with it: present it differently, extract tables of contents, index it, and so on.
CSS
There were attempts to expand the styling capabilities of HTML with tags like <font>
, but a better solution arrived a few years later in 1996: CSS.
CSS enabled the separation of content and presentation by using style sheets. CSS relied on rules (e.g. h1, h2, h3 { font-family: sans-serif }
) to define styles for HTML markup. It was generally easy to understand, except for the concept of “specificity”, which caused countless headaches for generations of web developers. As a result, a number of workarounds such as BEM and Tailwind emerged over the years.
JavaScript
As the final cornerstone technology of the web, the confusingly named JavaScript actually arrived shortly before CSS, but became a standard under the name ECMAScript later in 1997.
Despite the name, JavaScript has virtually nothing to do with Java except for a somewhat similar syntax. Unlike Java, JavaScript has dynamic typing, prototype-based object-orientation, and first-class functions (Java added support for anonymous functions much later).
JavaScript was designed to add dynamic behavior to otherwise static web pages, but despite its many strengths, it was arguably the world’s most misunderstood language for quite some time.
Ajax
JavaScript received a massive boost of popularity from Ajax, a browser technology that allows a web page to exchange data with servers in the background. Before Ajax, many user actions like submitting a form required a complete page refresh. Ajax enabled the separation of data exchange and presentation, and dynamic web apps as we know them today were born!
The first popular web apps started to appear in the mid-2000s, with Google’s Gmail and Maps as prime examples.
jQuery
As web apps started to appear, the tooling for web apps was seriously lacking. Enter jQuery, a JavaScript library which allowed developers to use CSS selectors to access any DOM element. jQuery supports a large number of chainable methods for DOM manipulation, event handling, and Ajax.
Here’s a typical piece of jQuery code. It’s probably not hard to guess what it does:
$('ul.tablist li').addClass('tab').on('click', selectTab);
Thanks to its simplicity and power, it’s no wonder jQuery is still widely used nearly two decades after its initial release. However, a major issue with jQuery is the close coupling of presentation (DOM elements) and JavaScript logic, which can make maintenance and scalability hard.
AngularJS
Starting with the 2010s, frameworks specifically designed to create Single Page Applications started to appear with the arrival of AngularJS.
AngularJS’s main selling point was two-way data binding. AngularJS keeps the data model and the corresponding HTML (view) in sync automatically. In other words, whenever one changes, the other changes too. This reduces the need for direct DOM manipulation via libraries like jQuery at the expense of additional complexity and potential performance issues introduced by the framework.
Here’s how a simple counter can be implemented in AngularJS:
<div ng-app="myApp" ng-controller="counterCtrl as counter">
<button ng-click="counter.increment()">Increment</button>
<div>Count: {{ counter.count }}</div>
</div>
<script>
angular.module('myApp', []).controller('counterCtrl', function () {
this.count = 0;
this.increment = function () {
this.count++;
};
});
</script>
React
React, which emerged a few years after AngularJS, is now the most popular web frontend library.
Unlike AngularJS, which uses the existing HTML as a template, React components dynamically generate HTML using JavaScript. JSX, a JavaScript syntax extension, is typically used for HTML generation but requires a transpiling step before use.
The creators of React argued that “instead of artificially separating technologies by putting markup and logic in separate files, React separates concerns with loosely coupled units called ‘components’ that contain both.”
Under the hood, React utilizes a virtual DOM to optimize updates, altering only the parts of the browser DOM that have changed, thus enhancing performance. There’s no direct support for two-way data-binding, but it’s possible to achieve a similar result by other means.
The following example is a counter implemented in React with hooks, a later addition to the library:
import React, { useState } from 'react';
const App = () => {
const [counter, setCounter] = useState(0);
const increment = () => {
setCounter(counter + 1);
};
return (
<div>
<button onClick={increment}>Increment</button>
<div>Count: {counter}</div>
</div>
);
};
export default App;
Vue.js
Shortly after React came out, Evan You created Vue.js with the goal of creating a lightweight version of AngularJS. It’s currently the second most-popular web frontend framework, following React. Just like React, Vue internally uses a virtual DOM, but it also supports two-way binding.
Vue supports single-file components (SFCs), which encapsulate HTML templates, JavaScript, and CSS in one file, keeping them clearly separated. The following example is in SFC format, using Vue’s original Options API. Vue also supports a new API called “Composition” that works similar to React hooks.
<template>
<button @click="increment">Increment</button>
<div>Count: {{ count }}</div>
</template>
<script>
export default {
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count++;
},
},
};
</script>
<style scoped>
button {
color: blue;
}
</style>
Other popular web frontend frameworks include Angular, which is quite different than the original AngularJS, and Swelte.
Bundlers
Web bundlers such as esbuild and webpack are often used to manage the varying levels of support for JavaScript and CSS features across web browsers. These tools transpile modern JavaScript and CSS code to formats old web browsers can understand.
Bundlers also decrease the size of JavaScript and CSS code by performing tasks like tree shaking for unused code elimination, removing whitespace, and shortening variable names.
Bundlers typically consolidate hundreds or even thousands of JavaScript and CSS files from a project into a few optimized files for easier distribution.
Until recently, using web bundlers as code transpiling tools was mostly a necessity, but with the retirement of Internet Explorer 11, modern evergreen browsers (Chrome, Edge, Firefox, and Safari) no longer require transpilation.
Progressive Enhancement
The idea of progressive enhancement is about prioritizing HTML content over everything else. Progressive enhancement has been around for quite some time, but gained renewed attention with the proliferation of frontend frameworks.
Not all web apps are single-page applications, and not all single-page applications are complex enough to warrant a full-blown frontend framework.
Starting with HTML and CSS, and enhancing them with lightweight JavaScript frameworks like Alpine.js and htmx offers numerous advantages, including faster initial load times, better accessibility, easier indexing of page content by search engines, and reduced or no need for bundlers.
Here’s our counter implemented in Alpine.js:
<div x-data="{ count: 0 }">
<button @click="count++">Increment</button>
<div>Count: <span x-text="count"></span></div>
</div>
Although Alpine encourages “sprinkling” JavaScript into HTML as a form of progressive enhancement, the above example can be expanded into a reusable component, as shown below:
<div x-data="counter">
<button @click="increment">Increment</button>
<div>Count: <span x-text="count"></span></div>
</div>
<script>
const app = () => {
Alpine.data('counter', () => ({
count: 0,
increment() {
this.count++;
},
}));
};
document.addEventListener('alpine:init', app);
</script>
Too Much of a Good Thing
We’ve come a long way since the early days of the web, yet we are still using the same three core technologies: HTML, CSS, and JavaScript, all of which have vastly improved over the past three decades. To give a few examples, HTML now natively supports web components, CSS layouts have been enhanced by flexbox and grid, and JavaScript proxies have simplified the implementation of two-way binding.
Despite this progress, it seems we rely too much on JavaScript, the most powerful of the three core web technologies. Had Tim Berners-Lee chosen a more powerful language as the foundation of the web, instead of HTML, would the web have gained as much traction? How would search engines even work? They couldn’t merely parse web pages; they would need to execute code and evaluate the output somehow. Does anyone even remember websites built entirely with Flash, let alone Java applets?
Coming Full Circle
As much as I like JavaScript, I think it’s time to return to the basics. I find it refreshing to write, not generate, HTML and CSS, and then sprinkle some JavaScript for interactivity. I’m sure many web developers would feel the same. Let’s give it a try.
Related: