Skip to content

Chapter 19: The Browser Caught Up While We Were Abstracting It

This is the hinge.

For the first eighteen chapters, we’ve followed the frontend stack upward. Documents. CSS. JavaScript. Standardization. Browser engines. Plugins. Ajax. jQuery. The structure libraries. The MVC frameworks. Mobile and responsive design. React. The reactivity counter-argument. The meta-frameworks. The runtimes. The dependency ecosystem. The web-as-native-application story.

Each layer answered real constraints. The browser was limited. Networks were slow. APIs were inconsistent. UI synchronization was hard. Full-page reloads broke continuity. Applications needed routing, data loading, state, rendering, and deployment structure. We built abstractions because there was nothing else to build with.

The browser didn’t stand still during any of this.

While the frontend ecosystem layered more architecture above the platform, the platform itself grew stronger. It absorbed patterns. It standardized APIs. It became more consistent. It added capabilities that once required libraries, plugins, or heavy framework code. The work the standards bodies did between roughly 2015 and now is, in this book’s argument, one of the most important and least appreciated developments in computing. It’s the work the rest of this book is going to take seriously.

The layers we’ve discussed — React, Apollo Client, GSAP, Next.js, the whole accumulation — seem like magic until you look beneath the hood and realize they’re incredible feats of engineering built on top of the operating system the browser engine provides. That sentence is, in some ways, the thesis of this book in one line. The frameworks are not magic. They’re cathedrals built on a substrate that, while the cathedrals were going up, quietly became its own foundation.

The question this chapter — and this book — has to ask is one the industry generally avoids:

Are we still building for the browser we used to have?

Many frontend defaults were formed under older constraints.

We learned to avoid direct DOM APIs because they were inconsistent or verbose. We learned to replace native controls because they were hard to style. We learned to render everything with JavaScript because full-page reloads felt terrible. We learned to hide forms behind state libraries because custom validation and async submission were easier inside framework code. We learned to use routers because the browser’s navigation model was too page-oriented for app-like experiences. We learned to use build tools because JavaScript had no native module system. We learned to ship component libraries because native elements weren’t enough for product UI.

Some of those lessons still hold. Some have decayed into half-truths. Others are now actively bad advice — and we’ll work through a few by name.

The difficulty is that habits outlive constraints.

Plenty of codebases still wrap every native <button> in a custom <Button> component because, somewhere around 2016, a design system needed a hover state the target browser didn’t render correctly. The browser was fixed years ago. The wrapper is still there, in every page, in every product. Plenty of codebases still use react-modal instead of <dialog> because, in 2018, native dialog didn’t have good focus-trap behavior. The platform shipped the fix in 2022. The library is still there. Plenty of codebases still ship Webpack configurations that bundle CommonJS modules into a synthetic ESM environment because, in 2016, native modules didn’t work reliably in any browser. The platform shipped reliable native modules in 2017–2018. The Webpack config is still there.

These decisions are rarely malicious. They’re inherited. They were correct when they were made. They’re the kind of thing a thoughtful developer never quite gets around to revisiting because the existing solution works, the codebase has bigger problems, and if it ain’t broke, don’t fix it is a reasonable engineering principle most of the time.

The cost shows up in aggregate. A modern frontend application carries, on average, several years of inherited decisions made against constraints that no longer apply. Each decision is small. Together, they account for a substantial fraction of the bundle size, the build complexity, the test surface, and the architectural assumptions a team has to maintain. Rechecking those decisions, with the modern platform as the baseline, is one of the most leveraged exercises in modern frontend work.

This chapter is the start of that exercise. The rest of Part II is the rigorous version.

Between 2015 and now, browsers shipped a lot.

ES2015 was the language inflection. Native modules, with import and export. let and const. Arrow functions. Classes. Promise. Template literals. Destructuring. Map, Set, WeakMap, WeakSet. Symbol. Generators. The annual cadence after that added async/await (2017), Object.values/Object.entries (2017), Array.prototype.includes (2016), the spread and rest operators (2018), optional chaining (2020), nullish coalescing (2020), top-level await (2022), and dozens of smaller improvements. The language we write today is structurally different from the language of 2014, and the difference is the result of TC39’s patient annual work described in Chapter 5.

fetch shipped in Chrome and Firefox in 2015 and reached general browser support by 2017. AbortController, the missing companion that lets a fetch request be canceled, shipped in 2017–2018. The combination replaced XMLHttpRequest and the wrapper libraries that had grown up around it ($.ajax, axios, superagent) for most use cases.

Custom elements v1 (the standardized version of web components) reached general browser support in 2018. Shadow DOM v1 shipped alongside it. The HTML <template> element matured. The platform finally had a real component primitive — a way to define a new HTML element with its own behavior, lifecycle, and rendering, registered in the browser the same way the platform’s built-in elements were.

The three observers — MutationObserver (2014), IntersectionObserver (2016), ResizeObserver (2018) — gave the platform a clean way to react to changes in the DOM, in scroll position, and in element sizes, without manual polling. The previous generation of solutions (setInterval loops, scroll event handlers, layout-thrashing techniques) became unnecessary.

The History API, introduced in 2010, matured through the late 2010s and became the foundation for client-side routing without breaking the browser’s address bar.

FormData, URL, and URLSearchParams gave the platform native APIs for the data structures that web applications were constantly manipulating. The Constraint Validation API and ElementInternals (2020) made native form validation and form-associated custom elements usable for serious applications.

<dialog>, native modal dialogs with proper focus management, became reliably supported around 2022. The Popover API, for non-modal overlays, shipped in 2023–2024. The combination meant that, finally, custom modal libraries weren’t necessary for most cases.

CSS custom properties (CSS variables) shipped in 2016. Cascade layers shipped in 2022. Container queries shipped in 2022–2023, as Chapter 12 described. The :has() selector shipped in 2022–2023. The View Transitions API — work led by Jake Archibald and Bramus Van Damme on Chrome’s team — shipped in 2023 and is gradually reaching other browsers, with the cross-document version landing in 2024–2025.

Service workers (the platform’s offline-and-caching layer) and web workers (for true parallel JavaScript) reached general use through the late 2010s. The Storage Access API, the Cache API, IndexedDB, and OPFS gave the platform a tiered storage hierarchy that, taken together, replaces the need for most third-party state-management solutions for client-side persistence.

BroadcastChannel, for cross-tab communication, shipped in the late 2010s. structuredClone made deep object copying a one-line operation. The Crypto API gave the platform first-class cryptographic primitives. The Performance API gave it real measurement tools. WebGL and later WebGPU gave it GPU access. WebAssembly arrived in 2017 and now provides a second runtime that can run code written in any language at near-native speed.

The accessibility platform also matured. The accessibility tree became more reliably exposed. ARIA support became more consistent across browsers. The interop work between browsers, screen readers, and authoring tools steadily reduced the cross-browser accessibility differences that had made the surface so frustrating in the 2000s and early 2010s.

The Interop initiative, started by the browser vendors in 2022, produced explicit annual targets for closing remaining cross-engine differences. Interop 2024 closed major gaps in popovers, scroll-driven animations, anchor positioning, and custom select elements. Interop 2025 continued the work. The progress is publicly tracked at wpt.fyi and is measurable.

Most of the defaults we still teach were formed when none of this existed.

That’s the chapter’s central observation, and it’s worth letting it land. The frontend industry’s organizing assumptions — that the platform is weak, that the DOM is hard to work with, that custom solutions are needed for everything from layout to modals to form validation to navigation to component composition — were correct in 2010. They were partially correct in 2015. They’re mostly wrong in 2025. And the working developer who learned the field between roughly 2014 and 2018 has internalized those assumptions as facts about the universe, when they were actually facts about a particular browser landscape at a particular moment in computing history.

A short tribute, because the work was done by people.

The W3C working groups, the WHATWG, and TC39 have been described throughout this book. The specific platform features named above were each driven by a small group of engineers across multiple companies, often working in standards groups for years before their work shipped.

Custom elements and shadow DOM are largely the product of work led by Dimitri Glazkov, Alex Russell, Steve Orvell, and the Polymer/Lit team at Google, with significant contributions from Apple, Mozilla, and independent participants. The web components effort took roughly a decade from first proposal to general adoption.

View Transitions is the work of Jake Archibald (who has been a presence in the standards community for over a decade, known for clear technical writing and patient advocacy), Bramus Van Damme, and the Chrome team. The API’s design closed a major ergonomic gap for the routing-belongs-to-the-server argument the rest of this book builds.

Container queries and the modern CSS layout system are the work of the CSS Working Group, with particular contributions from Miriam Suzanne, Rachel Andrew, Una Kravets, Adam Argyle, Elika Etemad, Tab Atkins, and the rest of the standards-engaged CSS community. Container queries took seven years from proposal to ship.

The Interop initiative is coordinated by representatives from Apple, Google, Microsoft, and Mozilla, with Igalia contributing across all four engines as one of the independent engineering consultancies that quietly underwrite a substantial fraction of the modern web platform.

TypeScript’s structural-typing-on-top-of-JavaScript design is Anders Hejlsberg’s work, building on his lifelong career in language design. The library that became TypeScript made the rest of the modern frontend ecosystem possible to write at scale.

These names are a partial list. The full list runs to hundreds of engineers, designers, and standards participants who’ve been doing slow, careful, mostly-unglamorous work for decades. The web platform you build for today is their collective contribution. Most of them aren’t household names. Their work is everywhere.

The pattern across all of these is consistent enough to be a working principle.

A capability appears as a real need in production applications. The ecosystem builds libraries to provide it. Multiple libraries converge on a working design. The standards bodies pick up the converged design. Browsers implement the standard. The capability becomes native. The library either disappears or shrinks to a thin polyfill for older browsers.

Selectors went through this cycle. jQuery’s selector ergonomics were the right answer to a real problem; querySelector made that answer optional.

Ajax went through this cycle. XMLHttpRequest’s rough edges drove the development of $.ajax, axios, superagent, and many others; fetch plus AbortController made most of those libraries optional.

CSS preprocessors went through this cycle. Sass, Less, and Stylus provided variables before CSS had them natively; CSS custom properties absorbed the use case (and added capabilities the preprocessors couldn’t match, since the variables now live at runtime and participate in the cascade).

Module systems went through this cycle. CommonJS and AMD established the model; ES modules made it native.

Widget libraries went through this cycle. Dijit, YUI controls, jQuery UI, and the React component ecosystem established the patterns; custom elements made the primitives native.

Animation libraries are going through this cycle now. GSAP, Framer Motion, Motion One, and the various scroll-animation libraries established the patterns; CSS animations, the Web Animations API, scroll-driven animations, and the View Transitions API are absorbing the use cases. GSAP and the others still earn their place for complex work; the cost-benefit math is shifting toward the platform.

Form-state libraries are going through this cycle. React Hook Form, Formik, Final Form, and the rest established the patterns; native form validation, ElementInternals, and form-associated custom elements are absorbing the use cases.

Component frameworks are arguably going through this cycle right now. React, Vue, and the rest established the component-as-unit-of-authoring pattern; custom elements (with Lit as the canonical decorating layer) provide the platform-native version of the same pattern. The framework ecosystem has not yet meaningfully shifted toward custom elements at scale, but the substrate now exists for that shift to happen.

The pattern doesn’t mean libraries are bad. It means libraries that solve problems the platform has subsequently solved are paying a tax — supply-chain risk, bundle size, churn — for capability the platform now provides for free. A reasonable engineering question to ask, with each library, is which problem is it solving, and does the platform now solve that problem too?

One of the easiest ways to misunderstand this book is to hear use the browser as never use abstractions.

That’s not the argument.

The browser provides primitives, not complete product architecture. Native <dialog> exists, but an application may still need a dialog module, design tokens, animation policy, accessibility tests, command integration, and focus conventions on top. Custom elements exist, but teams still need authoring patterns, packaging, theming, documentation, and interoperability conventions. Forms exist, but complex products still need validation flows, drafts, async submissions, retry policies, and error handling. CSS custom properties exist, but design systems still need token governance. Events exist, but applications still need event semantics, context, diagnostics, and privacy rules.

The point isn’t to abandon abstraction. The point is to build abstractions that respect the modern platform.

Lit, introduced in earlier chapters and discussed more in Part V, is the canonical example of how this works. A Lit element is still a real custom element. The platform’s contracts (events, accessibility, the DOM, CSS) all continue to apply. Lit decorates the platform with an opinion — a templating syntax, a reactive-property system, a simpler authoring API — without replacing what the platform underneath provides. Code written against Lit can be inspected, debugged, styled, and queried using all the standard platform tools.

Kitsune (introduced in Part III) follows the same principle in a different surface. It doesn’t say the browser has events, so no runtime is needed. It says the browser has events, so the runtime should build on event semantics rather than hide them. It doesn’t say native <dialog> exists, so no dialog component is needed. It says a dialog component should wrap native dialog carefully instead of rebuilding modality from <div>s by default.

Native-first isn’t no-framework. It’s better-framework.

If we were starting today — and the rest of this book takes seriously the question of what starting today would actually look like — we’d ask different questions.

For UI controls, the questions would be: Can this be a native element? Can the native element be styled to match the design? Can the native element preserve keyboard and accessibility behavior automatically? Does customization justify replacing the native behavior, with all the responsibility that entails?

For styling, the questions would be: Can CSS handle this with custom properties, cascade layers, container queries, or state selectors? Does JavaScript need to own this visual adaptation? Is the team currently using JavaScript for layout decisions that CSS could now express directly?

For interaction, the questions would be: Is this a link, a form, an event, or a command? Should the URL change when this state changes? Can the interaction progressively enhance — meaning, work usably without JavaScript and better with it?

For components, the questions would be: Should this be a web component? Should it be framework-specific? Does it need shadow DOM? What semantics does it expose to the accessibility tree, to forms, to events, to CSS? Could this component be reused across applications, or is it tied to a specific framework’s runtime?

For product capabilities — analytics, audit, observability, validation, permissions, notifications, design-system rules — the questions would be: Why is this code inside the component? Could it be a module observing events instead? Does the component need to know about Sentry, or does Sentry need to know about the events the component emits? Is the component the right home for this capability, or has the framework pulled it there by default?

For architecture, the questions would be: Where is the application’s context defined? Can the app explain why something happened, given a user action — or is the causality buried inside hooks and effects? Are events facts (something happened) and commands requests (something should happen)? What is the smallest runtime that would coordinate this work?

These questions are the bridge from history to proposal. Part II works through what the platform actually provides, chapter by chapter. Part III names the architectural principles that follow. Part IV builds a small runtime by hand to make the principles concrete. Parts V and VI introduce web components (via Lit) and Kitsune as the maintained framework that embodies the architecture.

The browser catching up doesn’t remove hard problems.

State is still hard. Distributed systems are still hard. Accessibility is still hard. Design systems are still hard. Data loading is still hard. Offline behavior is still hard. Internationalization is still hard. Security is still hard. Performance is still hard. Large teams are still hard. The hard problems of frontend engineering are largely essential complexity — problems intrinsic to building a usable application for real users on real devices in the real network conditions of the real world.

Modern browser APIs reduce some accidental complexity. The kind of complexity that comes from inconsistent implementations, missing primitives, and the cost of building everything from scratch each time. That reduction is significant and worth celebrating. It’s not the same as solving the essential problems.

This is why the answer can’t simply be vanilla JavaScript. Vanilla JavaScript is a material, not an architecture. The browser gives us strong primitives, but applications still need organization, abstraction, conventions, and conscious design. The question is what shape that organization takes — and whether the organization respects the platform underneath it or reinvents it in a parallel reality.

The new starting point isn’t a blank page.

It’s a browser with semantic HTML, powerful CSS, native events, real forms, custom elements, native modules, observers for size and visibility and mutation, the storage tier, service workers and web workers, the History API, accessibility primitives, and the standards bodies’ continued commitment to evergreen, backward-compatible evolution.

From that starting point, we can imagine a different architecture.

The DOM can be a context tree.

Attributes can be a protocol surface.

Events can be semantic communication.

Forms can be transactions.

CSS can be runtime adaptation.

Custom elements can be portable components.

Modules can provide product capabilities.

Boundaries can give application meaning.

A shell can own lifecycle.

A small runtime can coordinate events, commands, providers, diagnostics, and modules.

This is the path toward what the rest of the book argues for. Part II names each of these claims rigorously, with the platform’s actual APIs as the substrate. Part III draws the principles that follow. Parts IV–VI build the architecture in code.

But the move from history to architecture starts here.

The browser caught up while we were abstracting it. Now our architecture needs to catch up too.

Choose three frontend features you’ve built or used recently. Possible candidates: a modal dialog, a dropdown menu, form validation, a theme system, a page transition, a data fetch with cancellation, a tooltip, a toast notification, an autocomplete, a route change.

For each one:

  1. How did libraries or frameworks solve this historically? What was the canonical 2015 approach?
  2. What modern browser APIs or CSS features now support it? When did those APIs ship?
  3. What parts of the original library’s value still need abstraction? What parts have been absorbed by the platform?
  4. What parts of your current implementation might be habit — solving problems that no longer exist?
  5. How would you build this feature today if you were starting fresh with the modern platform?
  6. What accessibility responsibilities would you inherit if you replaced the native primitive with a custom solution? What responsibilities does using the native primitive give you for free?
  7. Could the feature be represented as an event, a command, a module, or a boundary, in the architecture this book is building toward?

The goal isn’t to delete your tools. Most of them still earn their place; some of them are doing exactly what the platform can’t do; many of them are doing what the platform now can. The goal is to make each tool justify itself against the browser we have now, rather than the browser the tool was originally written for. After this exercise, the rest of the book reads differently — because you’ll have done, for three concrete features, the rechecking that the platform-first argument depends on.

Part I closes here. Part II begins by taking the browser we have now seriously, one architectural concept at a time.