Chapter 4: JavaScript Adds Behavior
The web’s third layer is behavior. HTML gave the document structure. CSS gave it presentation. JavaScript gave it the ability to do things — to respond to a click, to update a value, to fetch new content without a page reload, to be a place where software runs rather than where documents are read.
Like the first two layers, JavaScript has an origin story worth telling.
In May of 1995, a researcher named Brendan Eich joined Netscape Communications. He’d been promised the chance to build a Scheme interpreter for the Navigator browser — a serious functional language for a serious new platform. Within weeks of arriving, that promise dissolved. Marc Andreessen and the rest of Netscape’s leadership had decided the browser needed a scripting language soon — to compete with Microsoft’s coming Internet Explorer, to give web pages programmable behavior, to make the platform feel less like a static document viewer. The language couldn’t be Scheme. It needed to look familiar to programmers who knew Java. And it needed to ship in ten days.
Eich built it in ten days. The first prototype was called Mocha. It became LiveScript when Netscape shipped the Navigator 2.0 beta. Then, in December 1995, as part of a co-marketing arrangement with Sun Microsystems (which owned Java and badly wanted to extend its brand into the browser), Netscape renamed the language one more time. LiveScript became JavaScript.
What followed was one of the most consequential ten-day projects in computing history.
By 1996 every major browser had a JavaScript implementation. Microsoft shipped JScript in Internet Explorer 3 — a separate, deliberately-not-quite-the-same reverse engineering of Netscape’s language. By 1997 JavaScript was important enough that Netscape submitted it to Ecma International for standardization, which produced the specification we now call ECMAScript. That standardization story is its own chapter (Ch 5). This chapter is about what JavaScript did once it was in the world, and what it changed about the web.
Enhancement Before Ownership
Section titled “Enhancement Before Ownership”In its original mode, JavaScript was a layer of enhancement on top of an already-working web. A form remained a form, but JavaScript could warn the user before submission. A list rendered by the server could be sorted or filtered without a round trip. A link could prefetch its target, or an image could lazy-load when the user scrolled. The page still worked without JavaScript, at least in some useful way — HTML still carried meaning, CSS still carried presentation, the server still produced documents, and the browser still owned navigation. JavaScript added convenience, speed, and refinement.
This was a powerful pattern because it respected the platform’s layers. The application didn’t have to wait for JavaScript to load before being useful. The page degraded gracefully when scripts failed or were blocked. Search engines, screen readers, and earlier-generation browsers could still read the content.
Enhancement has limits, though. Some experiences aren’t easily expressed as documents with small behavioral improvements layered on top. A spreadsheet, a map editor, a design tool, a code editor, a dashboard, a game, a collaborative workspace — these need a deeper client-side model. The browser must hold state. It must coordinate complex interactions. It must update continuously. For applications like these, JavaScript stops being an enhancement and becomes the substrate.
The risk in that transition is letting JavaScript erase everything the browser already knew. The platform has decades of accumulated answers — semantics, accessibility, navigation, focus management, history, forms — and a JavaScript-owned application that rebuilds the world from scratch typically rebuilds it badly. Modern frontend’s deepest mistakes are usually here: a JavaScript-owned application rebuilding things the platform already does well, instead of using JavaScript to coordinate and extend what’s there.
Direct DOM Manipulation
Section titled “Direct DOM Manipulation”The original JavaScript programming model was direct. Find an element. Listen to an event. Change the DOM.
<button id="toggle">Show details</button><div id="details" hidden> More information goes here.</div>
<script> const button = document.querySelector('#toggle') const details = document.querySelector('#details')
button.addEventListener('click', () => { details.hidden = !details.hidden button.textContent = details.hidden ? 'Show details' : 'Hide details' })</script>For small interactions this works beautifully. It’s readable, it uses the platform, it doesn’t need a framework, the state is tiny, and the behavior is local to the page that needs it.
Direct manipulation runs into trouble when multiple pieces of code need to coordinate around the same state. The disclosure panel that started as a clean click handler eventually needs to update an icon, record an analytics event, save the user’s preference to local storage, notify a sibling component, change the URL, support keyboard shortcuts, and emit a diagnostic event for debugging. Now the handler grows. Or, worse, several scripts begin manipulating the same DOM elements from different places, and the page becomes a set of invisible relationships nobody fully understands.
Direct manipulation is local. The moment behavior stops being local, the page needs architecture. The rest of frontend history is largely about what kind of architecture, and what each kind gives up in exchange.
Browser Inconsistency and Pain
Section titled “Browser Inconsistency and Pain”Modern developers sometimes underestimate how much early JavaScript work was shaped by browser inconsistency.
In 1996, Internet Explorer 3 shipped with Microsoft’s own implementation of JavaScript, called JScript. JScript was, by design, not quite the same language as Netscape’s JavaScript. Microsoft had reverse-engineered the implementation, made independent choices about edge cases, and added IE-specific extensions. The DOM was even worse. Netscape and Microsoft each had their own model for what an HTML document was supposed to look like to JavaScript — different ways to find elements (document.layers versus document.all), different event models, different ways to listen for clicks, different rules for how attributes worked. Writing JavaScript that worked in both browsers meant writing two implementations, or writing one implementation with elaborate runtime checks.
The W3C began to bring order in 1998 with the first DOM Level 1 specification. By 2000, DOM Level 2 added the addEventListener model that’s now standard. Standards lag adoption, though, and adoption lags shipped browsers. For most of the late 1990s and early 2000s, JavaScript developers were writing code that had to negotiate between competing browser implementations of the same supposed standard. Internet Explorer 6, released in 2001, sat in market dominance for most of a decade and quietly accumulated quirks that the rest of the platform would spend years working around.
This is the context in which the first generation of JavaScript libraries appeared. When jQuery, Prototype, MooTools, and Dojo show up later in this part of the book, they’ll be doing serious engineering against a hostile substrate — normalizing events, smoothing over DOM differences, providing reliable Ajax, giving developers an animation API that worked everywhere. The libraries that survived this era did so because they were honestly useful. Writing JavaScript without their abstraction was painful in a way that’s hard to remember now.
Abstractions have history. To evaluate an abstraction fairly, we have to know which browser it was built for, which inconsistency it was solving, and whether that inconsistency still exists.
State Appears
Section titled “State Appears”The simplest JavaScript enhancement may not need much state. As soon as behavior becomes interactive, state appears.
A counter has state:
let count = 0A disclosure has state:
let open = falseA form has state:
let dirty = falselet valid = truelet saving = falseA list has state:
let items = []let filter = ''let selectedId = nullOnce state appears, the UI must stay synchronized with it. If saving is true, a button should be disabled. If the form is invalid, an error should appear. If a row is selected, details should render. If a filter changes, the list should update.
Direct DOM code can handle this, but it gets harder as state relationships multiply. The central problem stops being how to change the DOM. The central problem becomes how to keep the DOM, the application state, the user’s intent, the server’s data, the validation rules, and the side effects all aligned with each other.
This is the synchronization problem that eventually leads to frameworks. React’s render-from-state model, Angular’s binding, Vue’s reactivity, Solid’s signals, Svelte’s compiler — they’re all responses to it. They differ in technique. They share a concern: how should the interface reflect changing state?
Behavior Can Damage Semantics
Section titled “Behavior Can Damage Semantics”JavaScript can enhance the platform, and it can also bypass it. A common pattern:
<div class="button" onclick="save()">Save</div>This looks like a button but isn’t one. It doesn’t support keyboard activation, doesn’t participate in forms, has no disabled behavior, has no automatic accessible role or name, can’t be reached by tab, and may not appear in the accessibility tree at all. It asks JavaScript to recreate what HTML already provided — usually less completely than the original.
The same pattern appears with custom dropdowns, modals, tabs, menus, accordions, inputs, and form controls. Sometimes custom behavior is genuinely necessary because the native element doesn’t have the capability or the visual flexibility required. Often it starts as a styling desire — the native button looks ugly, let me make my own — and ends as accessibility debt years later. The reason this pattern is so common is partly historical: for most of the web’s history, the platform made native elements very hard to style consistently, and the path of least resistance was building a fresh control from a <div>. The decision was usually about styling, not about semantics, and the accessibility cost was paid later — often by users who didn’t know who to blame for what wasn’t working.
The lesson is the central tension of JavaScript-enhanced UI: the more behavior JavaScript owns, the more browser responsibilities it inherits. A <button> element is a contract with the browser. A <div> with an onClick is a contract you’ll need to write yourself, and most teams write it badly the first time.
Modern frontend architecture works best when it uses JavaScript where JavaScript adds value while preserving native semantics wherever the native element will do the job. Kitsune’s component philosophy follows this directly. A button should still be a button. A dialog should use native <dialog> when it can. Disclosure should consider <details> and <summary>. Forms should remain forms. JavaScript should coordinate capabilities, not replace the elements that already have them.
The First Event Boundary
Section titled “The First Event Boundary”Even simple JavaScript introduces an important idea: the event boundary.
A user does something. The browser emits an event. Code responds.
button.addEventListener('click', () => { // respond to user intent})At first the response is local. The pattern is bigger than local callbacks, though. The browser already has a sophisticated event system with bubbling, capturing, delegation, cancellation, and custom events. Events can describe interaction at one point in the tree and be observed at another — without every participant knowing every other participant exists.
Modern frameworks often hide this behind callback props:
<Button onClick={save} />That’s useful, and it narrows the mental model. A callback says when this child does something, call this function. The browser’s event model says something happened in this tree, and interested ancestors may observe it. Those are different shapes. The callback assumes the parent knows what should happen; the bubbling event assumes some participant — possibly far up the tree — will care.
The bubbling-event shape becomes load-bearing when an application has many semi-independent capabilities that all care about the same kinds of user action. If a button click should be observed by an analytics module, an audit module, a debug overlay, and a save-draft module — and none of them should be imported by the button — the bubbling event model is how that works. The platform has had this affordance since the beginning. We just rarely used it as architecture.
JavaScript as Escape Hatch
Section titled “JavaScript as Escape Hatch”JavaScript became the platform’s escape hatch. Layout that CSS couldn’t express, controls that HTML didn’t have, transitions that avoided full navigation, local state, animation, validation, routing — the answer to each was JavaScript. Through the late 1990s, the 2000s, and right up through the 2010s, the question “how do I make the browser do X?” usually had the same answer.
This was empowering, and it placed too much responsibility in one place. The modern frontend stack is partly the result of repeatedly putting missing platform features into JavaScript libraries, then keeping those libraries even after the platform learned to do the same things natively.
That doesn’t mean JavaScript should shrink to nothing. Modern applications need JavaScript. The questions worth asking about a piece of JavaScript code are more specific than do we need it? Better questions: Is it enhancing native behavior, or replacing it? Is it coordinating capabilities, or compensating for a platform gap that has since closed? Is it preserving the meaning the markup is supposed to carry, or destroying it?
These questions become more important as the historical accumulation deepens. The shape of the right answer depends on what was originally being solved for, and what’s still being solved for now.
What This Means for Modern Frontend
Section titled “What This Means for Modern Frontend”JavaScript added behavior. Behavior is not architecture. A click handler isn’t a module system; a DOM mutation isn’t state management; a custom widget isn’t accessibility on its own. The earliest use of JavaScript shows both the power and the risk of browser programming. It can make documents interactive. It can also turn a structured document into an unstructured application heap.
Modern frontend needs JavaScript as a coordination layer, not as a vehicle for individual components to absorb every responsibility a real application has — analytics, audit, validation, permissions, storage, routing, error handling, design-system rules. That’s the path the next several chapters trace. The shape of the architecture this book eventually proposes is just what falls out of taking JavaScript’s coordinating role seriously while leaving the rest of the platform’s responsibilities where they already work well.
Before that architecture can be built, the language itself has to grow up. The JavaScript described so far is the JavaScript of 1995–2005: ten-day-prototype roots, browser fragmentation, a language nobody quite trusted as a serious tool. The story of how it became one of the most rigorously standardized languages in widespread use — through the slow institutional work of TC39 at Ecma International, and eventually through TypeScript layering structural typing on top — is the next chapter.
After that, the rest of the historical thread: the browser wars and the engine consolidation, the plugin era, the Ajax inflection, jQuery, the structure libraries, the framework eras, the runtimes underneath. Each chapter is a layer the field added, and a generation of engineers who added it.
Exercise: Add Behavior Without Taking Over
Section titled “Exercise: Add Behavior Without Taking Over”Return to the document app from the previous chapters.
Add JavaScript enhancements, but keep the app usable as HTML.
Implement:
- A disclosure toggle for optional form details.
- A character counter for a notes field.
- Client-side validation messaging that supplements the browser’s native validation.
- A small dynamic filter on a list.
Constraints:
- Keep the semantic HTML intact. Don’t replace
<button>with<div>. - Don’t break form submission. If the user submits the form, it should still work as a form.
- Don’t hide content in a way that makes it inaccessible. Use
hidden,aria-expanded, and the native form attributes where they apply. - The app should remain navigable and usable if every line of JavaScript is removed or fails to load.
Afterward, sit with these questions:
- Which enhancements felt natural? Which ones started creating state you had to track separately?
- Which behaviors depended on the underlying HTML being semantic? Where would they have broken if the markup were all
<div>? - Which behaviors would become hard if the app grew to ten times its current size? What would be the breakdown vector — too much state, too many event listeners, too many places that needed to know about the same value?
- Did your JavaScript enhance the document, or did it begin to own the document?
The purpose is to feel the boundary where enhancement begins to turn into application architecture — and to notice, while doing it, how much the native HTML and the browser’s own event model are still doing the heavy lifting underneath.