This and 90% of the Code was written by ai.
-
A browser-native component model
-
No framework runtime required
-
Built from four main APIs:
- Custom Elements — define new HTML elements
- Shadow DOM — encapsulate DOM and CSS
- HTML Templates — reusable HTML structures
- Slots — compose content
-
Example:
<company-button>becomes a real HTML element
-
Inputs
- HTML attributes
- JavaScript properties
-
Implementation
- JavaScript
- DOM
- Shadow DOM
- CSS
-
Outputs
- DOM events
- Custom events
-
Composition
- Slots allow consumers to provide HTML/content
Attributes / Properties
↓
Web Component
↓
DOM Events
- Yes — slots provide a native composition mechanism
- Named slots provide explicit extension points
- Default slots allow arbitrary child content
- Custom elements can be nested inside other custom elements
<user-card>
<img slot="avatar" src="/alice.jpg">
<strong slot="name">
Alice
</strong>
<p>
Senior Software Engineer
</p>
</user-card>The important point:
Web Components don't prevent composition — they provide a browser-level composition model.
Web Components deliberately provide a relatively small primitive.
They don't give you an opinionated solution for:
- Application-wide state
- Routing
- Data fetching
- Server-side application architecture
- Forms and validation
- Application-level composition
- Testing strategy
- Build tooling
- Large component ecosystems
This isn't necessarily a weakness.
Web Components solve the component boundary, not the entire application.
Frameworks provide a much larger ecosystem around components.
They give us:
- State management
- Routing
- Data fetching
- Forms
- Testing
- Developer tooling
- SSR / SSG
- Application architecture
- Large third-party ecosystems
- Established patterns and conventions
Web Components
↓
Browser-level UI primitive
React / Vue / Angular
↓
Application development ecosystem
| Web Components | React / Vue / etc. |
|---|---|
| Browser standard | Framework ecosystem |
| No framework runtime required | Runtime + tooling |
| Works with plain HTML | Application abstractions |
| Cross-framework | Deep framework integration |
| Custom Elements | Components + state + rendering |
| Shadow DOM | Routing / data / SSR / tooling |
| Small primitive | Complete application model |
The goal shouldn't necessarily be:
"Replace React with Web Components."
Instead:
Use Web Components as a durable component boundary and frameworks as the application layer.
A useful architecture is:
APPLICATION
┌───────────────────────────┐
│ React / Vue / Angular │
│ Routing / State / Data │
│ Application UX │
└─────────────┬─────────────┘
│
Web Component API
│
┌─────────────┴─────────────┐
│ │
Attributes Properties
│ │
└─────────────┬─────────────┘
│
DOM Events
│
Custom Element
│
Shadow DOM + Slots
- Framework owns the application
- Web Component owns the durable UI primitive
- The browser becomes the interoperability boundary
A component could expose a simple, framework-independent API:
<user-card
name="Alice"
role="Senior Engineer">
</user-card>With composition:
<user-card
name="Alice"
role="Senior Engineer">
<img slot="avatar" src="/alice.jpg">
<p slot="description">
Works on the platform team.
</p>
</user-card>Internally:
<user-card>
│
├── Shadow DOM
│ ├── styles
│ ├── layout
│ └── internal behavior
│
└── Slots
├── avatar
└── description
The consumer doesn't need to know how the component is implemented.
The Web Component needs to be registered with the browser.
For example:
// user-card.js
class UserCard extends HTMLElement {
// ...
}
customElements.define("user-card", UserCard);Then a React application imports the module:
// main.jsx
import "@company/web-components/user-card";Once the module executes:
customElements.define("user-card", UserCard);the browser knows what <user-card> means.
Then React can render it:
function UserProfile({ user }) {
return (
<user-card
name={user.name}
role={user.role}
/>
);
}The same Web Component can be consumed by:
React
Vue
Angular
Svelte
Plain HTML
Server-rendered HTML
Usually, no.
Don't automatically create:
ReactUserCard
VueUserCard
AngularUserCard
if they simply forward:
props → attributes
events → DOM events
That creates unnecessary abstraction and maintenance.
Web Component
│
┌────────────┼────────────┐
│ │ │
React Vue Angular
│ │ │
consume consume consume
directly directly directly
For example:
- Better TypeScript types
- Framework-specific event handling
- SSR/hydration integration
- Form integration
- Ergonomic property handling
- Compatibility with framework conventions
A wrapper should improve the developer experience, not merely hide the Web Component.
A design system could publish:
@company/web-components
│
├── user-card
├── company-button
├── date-picker
└── data-table
React:
import "@company/web-components/user-card";<user-card
name="Alice"
role="Engineer"
/>Vue:
import "@company/web-components/user-card";<user-card
:name="user.name"
:role="user.role"
/>Plain HTML:
<script type="module"
src="/web-components/user-card.js">
</script>
<user-card
name="Alice"
role="Engineer">
</user-card>For components that may need to survive for many years:
- Keep the public API platform-oriented
- Prefer attributes, properties, events and slots
- Avoid exposing framework-specific concepts
- Keep implementation details inside the component
- Treat the Web Component API as a stable contract
Stable API
│
┌───────────┼───────────┐
│ │ │
Attributes Properties Events
│ │ │
└───────────┼───────────┘
│
Web Component
│
┌────────┼────────┐
│ │ │
React Vue Plain HTML
They are a browser-native interoperability layer.
- Application architecture
- State
- Routing
- Data
- Tooling
- Ecosystems
- Developer experience
Web Components are the interoperability layer. Frameworks are the application layer.
For a long-lived design system, this gives us the possibility to:
Build once → publish a stable HTML API → consume from many frameworks → survive framework migrations.