Skip to content

Chapter 33: On Ergonomics, Taste, and the Right Kind of Abstraction

There’s an objection to the platform-first argument the book has been building. The objection is real, and it deserves a serious answer.

The objection: platform-first code is more verbose than framework code. A working React component is shorter than the equivalent vanilla-JavaScript-plus-custom-element. A Next.js application takes less code to ship than a server-rendered application with hand-rolled View Transitions. A Zustand store is a one-liner; the storage-and-event-wrapper version from Chapter 25 is fifty lines. If the alternative to React/Next/Zustand is writing more code to do the same thing, why is the alternative better?

The objection is sincere. The book’s argument doesn’t work if the answer is the alternative is worse but you should do it anyway for theoretical reasons. The answer has to engage with the ergonomics question directly. This chapter does that.

The response is four-part. Taste isn’t capability. The AI inflection changes the calculus. The decoration-vs-replacement principle. Altitude vs. all-or-nothing. Each of these reframes the question in a way that makes the original objection weaker than it first appears. Together they’re the architectural defense of the rest of the book.

This is also the chapter where the decoration-vs-replacement framing gets its formal name. The framing has appeared in earlier chapters as a working concept; here it becomes a named pattern the rest of the book uses consistently.

Start with the strongest version of the ergonomics objection, stated fairly.

A React component is dense, expressive, and easy to read. The component-as-function model maps cleanly to the developer’s mental model. JSX integrates markup and logic in a way that, after the initial learning curve, feels natural. Hooks compose. The ecosystem is enormous. Documentation is everywhere. Type definitions are universal. Hiring is straightforward. New developers can be productive in days.

function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</li>
))}
</ul>
)
}

The equivalent in raw DOM-with-event-listeners is longer. The equivalent as a custom element is longer. The equivalent in Lit is shorter than the custom-element version but still longer than the JSX. The React version is, line for line, the most concise expression of this is what a todo list looks like.

A working developer in 2025 looking at the React version next to a Lit version next to a raw-DOM version will, in most cases, prefer the React version. The preference is real, and it isn’t irrational. The terseness is real. The mental-model fit is real. The ecosystem advantages are real.

The book’s argument has to engage with this, not dismiss it.

The first part of the response is a clarification of what the ergonomics complaint actually measures.

A substantial fraction of the React version is easier judgments are the React version is more familiar. A developer who’s spent five years writing React reads JSX fluently. The same developer reading Lit or raw DOM has to slow down. The slowing-down feels like this is harder, when it’s actually I’m out of practice.

This isn’t a small effect. Programmer productivity is heavily influenced by familiarity. A developer who can write idiomatic Python in twenty minutes might take an hour to write the same thing idiomatically in Go, even if Go is the simpler language. The slowdown isn’t a property of the language; it’s a property of the developer’s experience distribution.

The familiarity effect is particularly strong in JavaScript frontend because the field has, for the past decade, organized hiring, training, education, and toolchain investment around React. Bootcamps teach React first. Onboarding guides assume React. Stack Overflow has more React answers than any other framework. The default mental model a working frontend developer brings to a new project is the React mental model. Of course React-flavored code feels easier; the developer’s brain has been shaped by React-flavored code for years.

The capability question is different. Can the alternative do what React does, given a developer with comparable experience? The answer, in most cases, is yes. Lit components express the same component logic as React components, in a different syntax, with comparable verbosity once familiarity is equalized. The Solid signal model is, after a few weeks, ergonomic in ways React’s hooks aren’t. Svelte’s compile-time approach produces code that, in the author’s hands, is sometimes the most concise of any framework.

The ergonomics objection, once familiarity is factored out, is weaker than it first appears. Some of the more verbose judgment is genuine — platform-first code does sometimes take more lines than framework code. Some of it is familiarity bias. Separating the two is the first move.

This isn’t a complete answer. There are real cases where platform-first code is more verbose, even after familiarity is equalized. The next three parts of the response address those.

Response 2: The AI Inflection Changes the Calculus

Section titled “Response 2: The AI Inflection Changes the Calculus”

The second part of the response is the one most often missed.

For most of frontend’s history, the human writes most of the code. The ergonomic question — how much code does the developer have to write — was the right question, because the human’s effort was the limiting factor. A terser framework saved the human time. The trade-off favored terseness aggressively.

The AI inflection changes this. In 2025, a substantial and growing fraction of the code in a working developer’s pull requests is generated by AI tools (Cursor, Copilot, Claude Code, similar systems) and reviewed by the developer rather than typed by them. The boilerplate is now cheap. The terseness saving has shrunk. What matters more, when the AI writes the first draft of the code, is:

Predictability. Code that does the obvious thing is code the AI can generate correctly. Code that depends on framework magic (a hook firing at the right time, a component re-rendering when the right thing changes, a context propagating through a tree the AI hasn’t fully traced) is harder for the AI to get right. The bug rate in AI-generated code, in our current generation of AI tools, is significantly worse for code with non-obvious framework semantics than for code that uses straightforward language constructs and platform primitives.

Consistency. Code that follows a uniform pattern across the codebase is code the AI can match. An application where every component uses the same shape — a custom element with reactive properties, slots for content, attribute-based metadata — is easier for the AI to extend than an application where each component is a unique hooks-and-context arrangement.

Type safety. Strongly typed code lets the AI catch its own mistakes. The TypeScript ecosystem already benefits from this; platform-native code with strict typing benefits even more, because the type system constrains what the AI’s output can do.

Debuggability. Code that’s debuggable through the browser’s standard tools — DOM inspection, breakpoints, network panel, accessibility tree — is debuggable by the developer reviewing the AI’s output. Code that requires framework-specific dev tools, framework-specific abstractions, and framework-specific mental models to understand is harder for the developer to verify when the AI gets something subtly wrong.

The shift in what matters is real, and it favors platform-first code in a specific way. Less abstraction means more transparency, which means more predictable AI generation and more reliable developer review. The terseness that used to favor React is partly redundant when the AI writes the boilerplate; the transparency that favors platform-first code becomes more valuable when the AI generates the code and the human has to verify it.

This isn’t a prediction. It’s an observation about how AI-assisted development is already changing the cost equation. The developer’s job is increasingly reviewing what the AI produced rather than writing it from scratch. The architecture that makes the review job easier is the architecture that wins, by an increasingly large margin.

For the next decade — and increasingly for AI-generated UIs themselves, where the model produces markup directly — the platform-first approach is the substrate that survives the transition. Frameworks built on parallel realities are harder for AI to generate against reliably; the platform’s primitives are well-understood, well-documented, and consistently shaped.

The ergonomics objection assumes the human is doing all the typing. That assumption is increasingly less true.

Response 3: Decoration vs. Replacement, Formally

Section titled “Response 3: Decoration vs. Replacement, Formally”

The third part of the response is the principle the book has been quietly building toward since Chapter 3 (Stylesheet Ecosystem) and used by name in Chapters 14, 17, and 22.

The principle: an abstraction can either decorate a platform primitive with an opinion, or replace the platform primitive with a parallel reality the abstraction owns. The two have different cost profiles, and the choice has architectural consequences.

A decorating abstraction adds an opinion to a platform primitive without hiding it. A wrapped <form> with sensible defaults still produces a real form. A custom element with extra behavior is still a real custom element you can target with document.querySelector, fire events on, style with CSS, observe with MutationObserver. The decoration is value-added; the underlying primitive is preserved. The abstraction uses the platform.

A replacing abstraction substitutes a parallel reality the library owns. A React-rendered <button> isn’t really a button you can interact with from outside React. The framework controls the rendering; bypassing it usually breaks something. The reality you program against is the framework’s, not the platform’s.

Both produce ergonomics. The cost profiles differ.

Decorating abstractions:

  • Inherit the platform’s contracts (accessibility, events, styling, navigation) by default.
  • Compose with other tools that target the platform (DOM queries, testing libraries, third-party widgets, AI-generated markup).
  • Survive framework changes — if the decoration goes away, the underlying primitive still works.
  • Have smaller bundle size, because the primitives don’t have to be re-implemented.
  • Are easier to debug, because the developer can drop down to the platform’s tools.

Replacing abstractions:

  • Have to recreate every platform contract they bypass (accessibility, focus, event propagation, native form participation).
  • Don’t compose well with non-framework tools — anything outside the framework’s reality has to go through framework-aware wrappers.
  • Bind the codebase to the framework’s continued existence and the framework’s evolution.
  • Have larger bundles, because the framework’s parallel reality is the runtime.
  • Are harder to debug — the displayed DOM doesn’t directly reflect the application’s state; the framework’s mental model is required to understand what’s happening.

The ergonomic trade-off — replacing abstractions feel terser because they’re more opinionated — is the strongest argument for the replacement model. The architectural cost — replacement abstractions create dependencies the application can’t easily shed — is the strongest argument against.

For applications with short time horizons and unambiguous framework choices, replacement abstractions are often fine. The trade-off is acceptable.

For applications with longer time horizons, multiple authoring styles (server-rendered, framework-rendered, custom-element-authored, AI-generated), or strong supply-chain concerns, decorating abstractions are better. The trade-off is less favorable for replacement.

This is the principle this book has been arguing for. Most of the rest of the book — Parts III, IV, V, VI — builds on decorating abstractions. Kitsune is, by design, a decorating architecture. Lit (introduced in Part V) is the canonical example of a decorating component library. The principle isn’t use no abstraction; it’s prefer the kind of abstraction that keeps the platform’s contracts intact.

A short tribute to the work that exemplifies the principle.

Lit is a small JavaScript library for building custom elements. The project grew out of Polymer, an earlier Google project for working with web components, and reorganized as a focused, modern library starting around 2018. Lit 1.0 shipped in 2019; the current Lit 3.x is the mature, stable version most teams use today.

The team behind Lit — Justin Fagnani, Steve Orvell, Kevin Schaaf, the Polymer team’s continuity into Lit — has spent the past decade building a component library that takes decoration as its design principle. A Lit element is a real custom element. It uses the platform’s native lifecycle (connectedCallback, disconnectedCallback, attributeChangedCallback). It uses the platform’s native event model. It uses the platform’s native styling (with Shadow DOM and slots when appropriate). It participates in forms through ElementInternals. It can be inspected with browser dev tools as any other DOM element.

What Lit adds is ergonomics. Reactive properties (@property) that re-render the template when they change. A template syntax (html\…`) that's more pleasant than manually building DOM nodes. CSS-as-template-tag (css`…“) for scoped styles. Type-safe property decorators. The library is small — under 10 KB minified and gzipped, including the templating engine — because it doesn’t re-implement what the platform already does.

A Lit component looks something like this:

import { LitElement, html, css } from 'lit'
import { property } from 'lit/decorators.js'
class TodoItem extends LitElement {
@property({ type: Boolean }) completed = false
@property() text = ''
static styles = css`
li { display: flex; gap: 0.5em; }
.done { text-decoration: line-through; }
`
render() {
return html`
<li>
<input
type="checkbox"
.checked=${this.completed}
@change=${this.toggle}
/>
<span class=${this.completed ? 'done' : ''}>${this.text}</span>
</li>
`
}
toggle() {
this.dispatchEvent(new CustomEvent('toggle', {
bubbles: true, composed: true,
detail: { text: this.text }
}))
}
}
customElements.define('todo-item', TodoItem)

The component is a real custom element. It uses Shadow DOM by default for style encapsulation. It emits a composed event for parent components to observe. Its properties are typed. The CSS is scoped. The render method is declarative. The element can be used in plain HTML with <todo-item text="Buy milk"></todo-item> and behaves correctly.

This is decoration at its best. The platform provided custom elements, reactive properties through observed attributes, Shadow DOM, slots, native events, and CSS. Lit adds the ergonomic layer that makes the platform pleasant to author against. If Lit disappeared tomorrow, the resulting custom elements would still work — they’re real platform elements, not framework constructs. The decoration is removable; the substrate is durable.

For the rest of this book — particularly Part V, where the Kit component library is built — Lit is the authoring layer. The chapter’s argument is that the architectural choice to use Lit is itself an example of the decoration principle. Most of what Lit gives you is ergonomics on top of the platform’s existing capabilities. The architecture pays a small cost (a 10 KB runtime) for substantial developer experience improvements, while keeping the platform’s contracts intact.

The chapter has to apply the same framing to React directly, without flinching.

React is a replacement abstraction in the precise sense the principle defines. The framework owns rendering. JSX produces virtual DOM, not real DOM. State management happens through hooks the framework controls. The component tree is the application’s runtime, not a thin layer on top of the platform’s existing structures. Code written against React only works because React is running; if the framework went away, the code would have to be rewritten against something else.

This isn’t a critique of React’s engineering. The framework is exceptionally well-built; Chapter 13 made that case at length. The point is structural. React’s design is to be the application’s runtime, not to decorate the platform’s. The trade-off is conscious — the team has been explicit, in various writings over the years, that the parallel reality is a feature, not an accident.

For applications that benefit from this trade-off — applications with rich interactive state, short time horizons, strong framework familiarity, and no need to integrate with non-React contexts — React is often the right choice. The chapter doesn’t argue that React is bad. The chapter argues that React represents a specific point on the abstraction spectrum, and that the point’s costs and benefits should be understood explicitly rather than assumed.

The supply-chain math (Chapter 17), the framework-breaking-change history (Chapter 11’s Angular parable, Chapter 13’s hooks transition), the AI-inflection considerations (Response 2 above), and the long-term-maintenance question all weigh on the trade-off. For some applications, the trade-off is favorable. For others, it isn’t.

The architectural lift the book is asking for is make the trade-off explicitly, with the principle named and the cost profile understood. Choose React knowingly, not by default.

The decoration-vs-replacement principle applies across the framework ecosystem.

Lit is the cleanest decoration. The component is a real custom element. The library is removable in principle. The platform’s contracts are fully preserved.

Svelte (Chapter 14) is decoration-via-compile-time. The compiler eliminates most framework abstractions before runtime. The components are real DOM elements with real CSS and real events. The runtime is whatever each component needs — small, scoped, and largely invisible at the application level.

Solid is closer to decoration. The component renders to direct DOM updates without a virtual DOM. The reactivity primitives are explicit. The framework’s footprint is small enough that the application doesn’t feel like a framework lives inside it.

Vue is mixed. The component model leans toward replacement (single-file components with their own conventions, virtual DOM for rendering). The reactivity (Proxy-based observation of plain JavaScript objects) leans toward decoration. Vue applications are usually debuggable with browser dev tools the way plain JavaScript can be.

Angular is firmly replacement. The framework owns the component model, the dependency injection, the change-detection cycle, the entire application runtime.

React is canonical replacement, as the previous section described.

Kitsune is decoration by design. The architecture sits on top of custom elements (via Lit). Boundaries are real DOM elements. Events use the platform’s event system. Storage uses the platform’s storage tier. The runtime is small enough that the application primarily talks to the platform, with the runtime providing coordination rather than abstraction.

The spectrum isn’t a quality ranking. Each point on it represents a different trade-off. The architectural argument is that the trade-off is visible — that a team choosing between frameworks should understand which point they’re choosing and what that choice costs and buys.

The fourth and final part of the response addresses what’s actually being argued.

The historical framing of the platform-vs-framework debate has been binary. Use React, or write vanilla JavaScript. Adopt Next.js, or build everything from scratch. Buy into the framework’s worldview entirely, or reject frameworks altogether. The framing is wrong in both directions.

The honest framing isn’t platform vs. framework. It’s altitude. At what level of abstraction do you want to author? Some altitudes are right for some applications and wrong for others. The choice is graduated.

A simple content site can author in raw HTML and CSS, with a touch of JavaScript for the interactive parts. The altitude is low. The code is direct. The platform handles most of the work.

A modest interactive application can author in custom elements with Lit, with a small architecture (modules, boundaries, events) for coordination. The altitude is moderate. The code uses platform primitives with a thin decoration layer.

A complex real-time application — Figma, Linear, Notion-class — might author in React, Vue, or another rich framework, with the framework’s full mental model as the substrate. The altitude is high. The code expresses application logic in framework terms.

These are all valid choices. The right choice depends on the application. Altitude vs. all-or-nothing means: pick the altitude that matches your application’s needs, rather than reaching for whatever altitude the field currently treats as default.

The platform-first argument isn’t write everything in vanilla JavaScript. It’s consider the lower altitudes more seriously than the field’s current defaults encourage. A team that habitually reaches for a meta-framework + React + a state library + a router + an animation library should ask whether the application actually needs all of that, or whether a lower-altitude approach would do the same work with less commitment.

For most applications most of the time, the lower altitude is a better fit than the field’s defaults suggest. The platform has caught up; the architectural argument is to take that catching-up seriously and to make the altitude choice deliberately.

After all four responses, what’s left of the ergonomics objection?

Some code is genuinely more verbose at lower altitudes. A platform-first implementation of a complex form will be longer than the React-Hook-Form equivalent. A custom-element-based component library will be slightly more verbose than the React equivalent, even with Lit. The verbosity is real, even after familiarity is equalized.

The ecosystem advantage is real for React. The number of libraries, the depth of documentation, the available examples, the hiring market — these are genuine advantages, and the platform-first approach doesn’t fully match them.

Some applications benefit from the trade-off React makes. The framework’s tight integration, the comprehensive mental model, the strong default opinions — these have real value for some products and some teams.

The remaining objection, after the four responses, is roughly: the platform-first approach is a serious option for many applications, but it’s not the right choice for every application, and the choice should be made with awareness of the trade-offs. That’s the position this book has been building toward. The book isn’t arguing for abandon React; it’s arguing for understand what choosing React (or not) costs and buys, and make the choice deliberately.

For the applications this book is mainly aimed at — applications that benefit from the platform’s evergreen guarantee, supply-chain resilience, long-term-maintenance posture, AI-readiness, and architectural transparency — the platform-first approach is the right default. For applications where those concerns matter less, the framework approach may still be the right call. The book’s job is to make the trade-off legible, not to make the choice for the reader.

Part II closes here. The browser we have now is a substantial application platform — HTML, DOM, attributes, events, forms, storage, navigation, CSS, animation, accessibility, internationalization, privacy/security, and the long-tail capabilities of WebAssembly, Workers, WebRTC, and hardware access. The decoration-vs-replacement principle gives the field a vocabulary for choosing how to build on top of it.

Part III names the architectural principles that follow from the platform substrate. Components and capabilities. Boundaries as application geography. Events, commands, and causality. The browser-native application loop. The chapters are shorter than Part II’s, because they’re naming patterns rather than introducing the platform. The patterns are what Part IV’s hand-built runtime and Part V’s component library will then implement.

The architecture the book has been pointing toward starts taking shape in Part III.

Exercise: Map Your Codebase on the Spectrum

Section titled “Exercise: Map Your Codebase on the Spectrum”

Pick an application or codebase you work on. Identify the major abstractions it depends on — UI framework, state management, routing, styling, data fetching, animation, validation, build tools.

For each one, place it on the decoration-vs-replacement spectrum:

  1. Does the abstraction decorate a platform primitive (preserves the primitive, adds an opinion) or replace it (substitutes a parallel reality)?
  2. If the abstraction disappeared tomorrow, would the application’s code still work against the underlying platform? Or would it have to be substantially rewritten?
  3. What’s the abstraction’s bundle-size cost?
  4. What platform contracts does the abstraction preserve? Which does it break?
  5. What altitude is the abstraction operating at? What altitude would the same work require if you wrote it directly against the platform?

Add up the picture. Where on the spectrum does your codebase sit, in total?

Then ask:

  1. Is that altitude the right one for your application’s actual needs?
  2. If you were starting today, would you choose the same altitude?
  3. What would change if you moved one or two layers lower — replaced a state library with platform storage, replaced a router with server routing, replaced a CSS-in-JS system with cascade-layered CSS?

The point isn’t to refactor everything. The point is to develop a working sense of where your application sits on the spectrum, and to notice the parts where the altitude is higher than the application’s needs require. Those are the parts where, over time, the platform-first approach gives you the most leverage — smaller bundles, easier maintenance, better AI-tooling compatibility, more durable code, lower supply-chain risk.

Part II ends here. The browser has caught up; the principle for building on top of it has a name. Part III is where the principles become architecture.