Web Components vs React for Design Systems
A matched input contract compares Web Components and React across properties, events, forms, styling, accessibility, SSR, and consumer migration.
Web Components vs React is not a referendum on which component model is better. For a multi-framework design system, the decision is where to place the distribution contract and which semantic differences consumers must carry.
This comparison implements the same labeled text input as a custom element and a React component. Properties, attributes, events, form participation, styling, accessibility, SSR, upgrade timing, and migration are tested against one contract so component interoperability becomes inspectable.
Frame Web Components vs React as distribution
The matched component is a labeled account-name input with value, default value, required and invalid state, help text, error message, input and change notifications, form submission, focus, and a style hook. The design system must serve React applications, another framework, and static or progressively enhanced pages. A React design system offers direct idioms and types to React consumers; custom elements offer a browser-defined registration and DOM boundary that multiple frameworks can instantiate.
Either implementation can be excellent or poor. The comparison asks where adapters, styling policy, server rendering, and ownership should live.
| Contract | Custom element | React | Proof |
|---|---|---|---|
| Value | Property + attribute | Prop + state | Initialization suite |
| Events | DOM dispatch | Handler contract | Consumer listeners |
| Forms | Internals or native | Native composition | Submit + reset |
| Style | Parts/tokens or light DOM | App styling | Override fixtures |
Match properties, attributes, and default state
HTML attributes are serialized strings or presence flags, while DOM properties can carry current values and richer JavaScript data. A custom element needs a documented reflection policy and careful upgrade handling when a property is set before definition. A React component receives props through the framework and decides controlled versus uncontrolled state.
Build fixtures for declarative markup, property assignment, attribute changes, default value, reset, and late registration. Avoid passing complex objects through JSON attributes unless that serialization is intentionally public. The shared contract should describe observable behavior without forcing one implementation's internal state model onto the other.
Runnable artifact: The inspectable contract matrix requires both candidates to expose the same seven public behaviors and proves that a missing form value is a real contract gap. Its component-boundary-contract.test.mjs receipt keeps the article's simplified boundary executable and reviewable.
Save the inspectable proof as component-boundary-contract.test.mjs and run node component-boundary-contract.test.mjs. Expected final line: PASS: component contracts compared.
import assert from "node:assert/strict";
const contract=["value-property","value-attribute","input-event","form-value","label-name","error-part","ssr-shell"];const web=new Set(contract),react=new Set(contract);const compare=(a,b)=>contract.map(x=>({name:x,a:a.has(x),b:b.has(x)}));const result=compare(web,react);assert.equal(result.every(x=>x.a&&x.b),true);web.delete("form-value");assert.equal(compare(web,react).find(x=>x.name==="form-value").a,false);console.log("PASS: component contracts compared");
Normalize events at the DOM boundary
Consumers need stable event names, timing, bubbling, composition, cancelability, and payload shape. A custom element dispatches CustomEvent or native-like events with deliberate options; React consumers may receive synthetic or DOM events depending on the boundary and version. Test from plain DOM and framework wrappers, including Shadow DOM where composition determines whether an event crosses the boundary.
Do not reuse a native event name with incompatible semantics. Prefer values available on the element or a documented detail object, and ensure duplicate controlled updates do not emit spurious change events.
Make forms and accessibility first-class
An input-like component must participate in labels, form value, validation, reset, disabled fieldsets, autofill expectations where relevant, and accessible descriptions. Form-associated custom elements can use ElementInternals in supporting browsers, but the support and fallback plan must be tested. React can compose a native input, which often makes semantics straightforward, yet wrappers can still break labeling or error announcements.
Compare the accessibility tree and keyboard behavior, not DOM shape alone. Keep the native control whenever it satisfies the need. Component interoperability is a failure if submitted form data or accessible names differ across consumers.
- 1Contract
Name behavior
- 2Implement
Build both inputs
- 3Consume
Test real frameworks
- 4Choose
Price migration + gaps
Choose a styling and encapsulation policy
Shadow DOM can protect internal structure and styles, but it also changes selector reach, theming, focus styling, event composition, and test queries. Expose custom properties, parts, slots, and documented states rather than asking consumers to pierce internals. Light DOM custom elements are also possible when global design-system CSS and application overrides matter more than encapsulation.
React components usually share the application's styling mechanism unless the library adds another boundary. Compare forced colors, reduced motion, directionality, zoom, and product overrides. The right choice is a supportable theming contract, not maximal isolation.
Test SSR, hydration, and custom-element upgrade
Server-rendered output should contain meaningful labels, values, and fallback semantics before JavaScript where the product requires it. React and its framework determine component server rendering and hydration behavior. A custom element can appear in server HTML, then upgrade when its class is registered; declarative Shadow DOM may participate depending on architecture and support.
Measure the no-JavaScript shell, layout shift, property replay, event readiness, and form behavior around upgrade. Do not claim SSR parity because both emit tag strings. The important question is what users and crawlers receive before and after runtime attachment.
Price wrappers and consumer migration
A cross-framework custom element may reduce duplicated rendering code while increasing adapter work for typing, event bindings, SSR, testing, and framework conventions. A React source can be fastest for a React-majority organization while requiring separate implementations or generated wrappers elsewhere. Build the same consumer story in each supported framework and count behavioral adapters, not lines alone.
Migration cost includes documentation, codemods, dual packages, visual regression, telemetry, and team learning. A hybrid system can use custom elements for stable low-level controls and native framework components for composition, but every seam needs clear ownership.
Choose from a shared browser contract
Run both inputs through the same contract: declarative and property initialization, value changes, events, labels, errors, form submission and reset, styling, keyboard behavior, server shell, and upgrade or hydration. Add consumers from the actual framework mix and record unsupported cases. Select Web Components when browser-level distribution and the proven adapter cost fit; select React when direct framework integration and organizational reality dominate; choose a deliberate hybrid when component categories differ. Revisit the choice as standards, React behavior, server architecture, or consumer share changes.
Evaluate the browser boundary in real consumers
MDN's Web Components guide, the HTML standard's custom-elements chapter, and React's custom HTML element reference describe the three sides of the public contract. Pair them with the journal's guidance on component API longevity, design-system adoption, supported escape hatches, and migration playbooks so the choice includes wrapper cost, version skew, and consumer change rather than stopping at demo interoperability.
Ship the same field into three consumers
Implement one accessible form field with value and default state, validation, label and description relationships, form participation, focus behavior, a public styling hook, SSR markup, and an analytics event. Consume it from plain HTML, the supported React version, and one second framework, then score wrappers and defects rather than demo lines of code. Web Components vs React has an honest answer when the same contract suite reveals where browser distribution removes duplicated work and where framework-native composition, typing, or rendering behavior remains worth a dedicated package.
Measure the wrapper as maintained surface area: translated props, event adapters, ref behavior, type declarations, SSR guards, test utilities, documentation forks, and release coordination. A Web Components vs React decision should count these obligations across the expected consumer years, because a tiny first wrapper can become the permanent place where browser and framework contracts drift.
Run the field contract with keyboard, screen-reader naming, autofill, native constraint validation, and programmatic form submission. This second Web Components vs React comparison prevents visual parity from hiding missing platform behavior and shows whether the distribution boundary actually reduces duplicated accessibility and form work for every supported consumer.
| Decision | Evidence retained | Stop condition |
|---|---|---|
| Frame Web Components vs React as distribution | consumer frameworks, component contract, support matrix, server-rendering need, styling boundary, package owner, and migration horizon | a framework preference replaces the representative consumer inventory |
| Match properties, attributes, and default state | public attributes, properties, reflection rules, default and current state, upgrade timing, reset behavior, and type declarations | the same name means a default in one candidate and live state in the other |
| Normalize events at the DOM boundary | event name, trigger, payload, bubbles, composed, cancelable, listener examples, and cross-framework test results | only the implementation's own demo can observe the change |
| Make forms and accessibility first-class | accessible name and description, keyboard flow, form value, validity, reset, disabled behavior, and tested assistive-technology assumptions | the component looks identical while one candidate disappears from submission or the accessibility tree |
| Choose a styling and encapsulation policy | DOM mode, token inheritance, parts or classes, slot policy, product override paths, accessibility media queries, and escape-hatch review | encapsulation prevents required adaptation or global CSS silently controls undocumented internals |
| Test SSR, hydration, and custom-element upgrade | server HTML, no-script behavior, upgrade or hydration timing, preserved properties, layout shift, event readiness, and failure fallback | an unresolved custom element renders an empty critical form until a large bundle executes |
| Price wrappers and consumer migration | consumer sample, adapter responsibilities, package and bundle impact, test reuse, documentation cost, codemod plan, and support owner | one proof-of-concept wrapper is extrapolated to an entire component catalog |
| Choose from a shared browser contract | shared contract results, consumer matrix, selected boundary by component category, rejected alternative, known gaps, and reopening signals | the decision is based on an isolated counter demo with no form, server, or migration evidence |
Web Components vs React should be chosen from shared behavior in actual consumers, not ideology. Repeat the contract tests when framework, browser, SSR, form, or migration assumptions change.