I’ve been spending the past few months juggling product engineering work with the building and scaling of design systems. While I can’t reveal much about how frontends at Amazon work behind the scenes, I can share some generalizable learnings that I believe will be useful to anyone trying to establish a reusable component ecosystem of their own.
To start, I’ll just focus on one specific topic: Picking the right framework for your design system.
React Is a (Very) Sensible Default
I’ve been working with React for about a decade now. I’ve seen the framework go through various phases:
- The early era of class components, accompanied by the somewhat awkward lifecycle methods API (e.g.
componentWillMount(),componentWillReceiveProps()) - The emergence of React Native and the tempering of some of the “write once, use everywhere” hype
- The push towards composition with functional components, hooks, concurrent mode, etc
For any web application that involves rich client-side interactions, the perks of using React and having access to the ecosystem of tooling built around it (e.g. Redux Toolkit, React Native, TanStack, Next.js, as well as component libraries like Mantine) are undeniable. Its declarative API for turning state into UI is also easy enough to pick up for most (even if it requires experience to master).
As indicated by 2025’s State of JavaScript survey, React continues to lead in terms of adoption. I’ve had the chance to work with Angular and Vue 2/3 at various periods of my career, but I’ve always preferred using React to ship features.
Without having context on the specific needs of an organization, I would strongly recommend React as a safe, reliable default framework of choice for getting started with building and shipping reusable components.
… But Your Situation May Be Different
If you work at a startup and are just looking to ship fast, you’re probably not pondering the question: Which framework is best for our design system?. Really, you just want to know: Which framework should we use to ship our Minimum Lovable Product?
The real demand for a design system often arises months or years into development, when your organization’s product and engineering scope grows to a point where simply letting individual teams drive their own UI development doesn’t scale well.
Such growing pains can manifest in several practical ways. It usually means needing to engage multiple teams just to ensure that:
- Basic UI Components (e.g. Buttons, Alerts) are rendered consistently with the same Design Tokens (e.g. colors, font, spacing)
- Composite UI Components (e.g. Product Cards, Chat Widgets, Payment Widgets) don’t require teams to do duplicate work
- App-level and Page-level UI Patterns (e.g. Page Shell Templates, Form Data Management, Navigation) are upheld consistently, such that the complex interactions within and between screens don’t throw off your customers
In particular, in companies where technical decision making is hyper-decentralized (i.e. “loosely coupled and loosely aligned”), these types of problems can easily take form.
The cost of migrating to React (or unifying towards any framework, for that matter) may be too much to bear for some. Organizational challenges and historical decisions can seriously impede the adoption of a design system.
It’s one thing to convince clients to migrate from one version of React to another, but it’s another thing to navigate more entrenched blockers (e.g. resource constraints, content management and rendering systems that don’t support React, legacy product flows that are stitched together with server-rendered markup and jQuery, etc).
With this in mind, it is sometimes helpful to not fixate on React (or any specific front-end framework, for that matter). Rather, one can turn to the browser spec.
Web Components Are Great for Interoperability
While adoption of React has surged over the years, Web Components have also come a long way. Most browsers now support the Web Components V1 spec. We’ve also seen the emergence of Stencil and the replacement of Polymer with Lit.
Using Web Components instead of React to ship design system components may help you navigate interoperability challenges. Web Components are registered to the Window object and accessible to the global DOM by default:
customElements.define("my-custom-element", MyCustomElement);Depending on the front-end landscape of your organization, different teams may have their UI templates defined using different technologies (e.g. React, PHP, JSP, etc). The neat thing about Web Components is that they are built to the browser spec, which makes them reusable across these frameworks.
Here’s an example of a React component using a custom web component internally:
import React from 'react';
import './user-card.js'; // Ensure the component is registered
const Profile = ({ user }) => {
return (
<div>
<h1>React Dashboard</h1>
{/* React passes data via attributes */}
<user-card name={user.name} theme="dark">
<p>This is a React-managed bio.</p>
</user-card>
</div>
);
};
export default Profile;And here’s an example of a JSP file also using the same custom web component internally:
<%-- profile.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<script src="user-card.js" defer></script>
</head>
<body>
<h1>JSP Dashboard</h1>
<%-- Assume 'user' bean is available in the request scope --%>
<user-card name="${user.name}" theme="dark">
<p>Logged in via Java/Spring Security.</p>
</user-card>
</body>
</html>As with most tools, there are always tradeoffs. In my opinion, the key thing with using Web Components over React is that you trade ease of development for interoperability.
… But Web Components Are Also Finnicky
Yes, Web Components require a lot of manual plumbing. This is natural, given they are built to match the browser spec. Lit and Stencil reduce this to some extent, but you still don’t get the full range of development abstractions from frameworks like React and Vue (e.g. virtual DOM).
A lot of the complexity of working with Web Components stems from the Shadow DOM. You will need to deal with:
- The Shadow DOM’s encapsulation of subtree nodes and styles: A generic
document.querySelectorcall will not return nodes inside a custom element. Global CSS classes will not influence their styles either, though there are exceptions. - Using
openandclosedmodes: The short (and perhaps over-simplified) answer is thatopenmode is usually desirable, since it exposes the.shadowRootand allows you to re-run queries within the subtree. This gives you access to the nodes and their properties (e.g.innerText), which can be useful for testing. Consumers will have to be intentional about overriding subtree nodes if they want. - Relating Properties to Attributes: In React, we mostly only care about
propsandstateand don’t have to think too much about defining them as class properties. With Web Components, we are dealing with HTML Elements that have both properties and attributes, and we need to have an idea of how to relate them (e.g. whether to use two-way or one-way binding for data, such as avalueattribute that gets updated by an input event). - Slot Change Handling: While child updates may trigger re-renders in React, the same cannot be said for Web Components. Any coupling between the slot container and the slotted container has to be managed with slotchange handlers, since the slot container typically operates in the Shadow DOM even while the slotted content remains in the Light DOM.
- Writing Encapsulated CSS: You will need to apply the
:hostselector to your CSS and have an understanding of which styles are scoped to the Shadow DOM, and which are not. Escape hatches, such as an exemption of properties likefont-family, and CSS variables, allow for styles to be passed from the Light DOM to the Shadow DOM, which is useful for consistency and theming. - Accessibility Gotchas: Given that
ids are unique within the Shadow DOM, you will have trouble using theforattribute to relate<label>s to<input>s, unless they exist within the same subtree. Communication between<form>elements in the Light DOM and<input>s within the Shadow DOM is also limited. - Consumer Plumbing: As a consumer, event handling may not be as simple as passing
onXyzChangedirectly to the Web Component, as you would do with a React “prop”. You will need write your own event listeners. A few things help to address this: Lit and Stencil both offer helpers that wrap your Web Components as React components, while React 19 fully supports Web Components such that you can directly pass your complex data and event handlers directly to Web Components.
In essence, the Shadow DOM helps to provide a layer of encapsulation over our custom elements, but this encapsulation comes with gotchas that developers without experience can easily trip on. LLM-assisted development will go a long way in mitigating these blindspots.
Consider a Hybrid Approach
In an ideal world, teams would share the same dependency tree and the need to address interoperability challenges would be minimal.
Clearly, we don’t live in an ideal world, so some amount of interoperability is typically required. In the event that your organization uses different front-end frameworks to generate different types of templates (PHP, JSP, React), a hybrid approach may be ideal. This usually means two things:
-
Creating stateless UI elements using Web Components. You would maintain, in a distinct package, a set of Web Components for stateless UI primitives like your Button, Modals, and Alerts.
-
Handling the assembly of these stateless UI elements in React, or any other team’s framework of choice. You would let your consumers import your reusable components package. Your consumers can then reference the HTML tags corresponding to your stateless elements in their respective UI templates (
<my-button>,<my-modal>, etc).
A middle of the road approach like this allows you to make firm decisions on core design system concerns like typography, colors, and sizing, while giving the consumers of your components the room to own their stateful logic for matters such as user interactions (e.g. form inputs) and side effects (e.g. API requests).
… But Consolidate If You Can
Personally, I would still advise teams to unify their front-end development around a single framework. For large engineering organizations, in particular, the merits of using a consistent set of tools far outweigh the potential upside of experimentation. Your code and patterns become more predictable, which also makes it easier to build custom AI agents to do larger-scale refactors.
There is a time and place for experimentation. This can be limited to “Labs teams” working on special projects or pages with unique data requirements. Upgrades to new major versions are also worthy of consideration. Experimenting with entirely new frameworks, on the other hand, is seldom worth it — the development benefits need to be extremely compelling to justify the long-term cost.
All that said, I know that the reality on the ground is often different for many organizations. This is where the value of a hybrid approach lies.
This post was also published on dev.to.