Learn what Web Components are, their advantages, naming rules, and how to build a reusable design system that works seamlessly in React, Vue, Angular, WordPress, and plain HTML.
π§© What Are Web Components?
Web Components are a set of native browser APIs that let you create custom, reusable HTML elements β your own tags β with encapsulated structure, style, and behavior.
Example:
<user-card name="Alice"></user-card>
it behaves like a real <div> or <button>, but defined entirely by you.
π Key Features of Web Components
| Feature | Description |
|---|---|
| π§± Custom Elements | Create your own HTML tags (<my-card>, <todo-list>) |
| π Shadow DOM | Encapsulates styles and DOM β no CSS leakage in or out |
| π§© HTML Templates | Reuse HTML chunks easily |
| π ES Modules | Import/export Web Components as modern JavaScript modules |
| β»οΈ Reusability | Use the same component anywhere β no framework needed |
| βοΈ Lifecycle Callbacks | connectedCallback, disconnectedCallback, etc. for lifecycle control |
| π Native Support | Runs in all modern browsers (Chrome, Firefox, Edge, Safari) |
βοΈ Official Naming Rules for Web Components
naming Web Components has a few strict rules defined by the HTML Custom Elements specification to ensure they donβt conflict with native HTML tags.
β 1. Must contain a hyphen (-)
This is mandatory.
-
β my-button
-
β user-card
-
β app-todo-list
-
β button (invalid β conflicts with
<button>) -
β todo (invalid β no hyphen)
π‘ The hyphen prevents clashes with existing or future native HTML tags.
β 2. Cannot start with certain reserved prefixes
Avoid starting with:
- xml-
- xsl-
- svg-
These are reserved for system use and XML-based namespaces.
β 3. Must be all lowercase
Custom element names are case-insensitive, so always use lowercase:
- β user-profile
- β UserProfile
β 4. Cannot contain spaces or special characters
Only letters, digits, and hyphens are allowed.
- β app-nav-bar
- β app_nav_bar
- β app.nav.bar
β 5. Cannot be a single word
A single word (like <todo>) is invalid β it must be compound (e.g., <todo-item>).
β 6. Should be semantically descriptive
Although not required, good names follow a pattern like:
<namespace>-<component>
Examples:
- app-header
- shop-cart-item
- ui-modal
- user-avatar
- profile-card
This makes your components more understandable and avoids naming collisions in large projects.
π§© Example of Correct Usage
class UserCard extends HTMLElement {
connectedCallback() {
this.innerHTML = `<div>Hello, ${this.getAttribute('name')}</div>`;
}
}
customElements.define('user-card', UserCard);
β Valid tag:
<user-card name="Alex"></user-card>
β οΈ Invalid Examples
| Name | Why invalid |
|---|---|
button | Conflicts with native tag |
UserCard | Must be lowercase |
app_card | Underscore not allowed |
app | Needs a hyphen |
xml-widget | Reserved prefix |
π§ Bonus Tip:
If youβre publishing components (e.g., via npm), itβs a good practice to namespace them:
- myapp-button
- myapp-card
- myapp-list
β Advantages (Pros)
1. Framework-agnostic
You can use Web Components with any frontend stack β Vue, React, Angular, or even plain HTML.
π§ Theyβre just native HTML elements, so any framework that renders HTML can use them.
2. Encapsulation
- CSS and DOM are completely isolated thanks to the Shadow DOM:
- No global CSS conflicts
- Safe to use in any environment (e.g., WordPress themes)
3. Reusability
Once built, your component can be dropped into:
- a static HTML pag
- a React app
- a WordPress block
- a Webflow embed
- Without rewriting or refactoring.
4. Performance
Because theyβre browser-native, they have no framework runtime overhead. The browser handles everything directly β lightweight and fast.
5. Future-proof
Web Components are part of the web platform itself β not tied to a specific framework version or library lifecycle.
6. Interoperability
They work across frameworks:
- Vue 3 β
<my-widget></my-widget> - React β
<my-widget /> - Angular β
<my-widget></my-widget> - WordPress β
<my-widget></my-widget>
β οΈ Disadvantages (Cons)
| Drawback | Description |
|---|---|
| π§ Complex State Management | No built-in reactive state like React/Vue β must handle manually |
| π§© Tooling Ecosystem | Fewer tools, plugins, and patterns than big frameworks |
| π§΅ Communication | Data passing (props, events) can be more verbose |
| π§° SSR (Server-Side Rendering) | Not natively supported β frameworks handle SSR better |
| π§ Learning Curve | Understanding Shadow DOM, slots, lifecycle takes time |
| β‘ Older Browser Support | IE11 doesnβt support it (requires polyfills) |
π§ When to Use Web Components
β Great for:
- Design systems / UI libraries (e.g., reusable buttons, modals)
- Embeddable widgets (e.g., chat widgets, payment forms, dashboards)
- Multi-framework projects (e.g., company uses Angular + Vue + React)
- CMS integrations (WordPress, Webflow, Wix, etc.)
- Web apps that need reusable vanilla components
β Not ideal for:
- Apps requiring deep state management (React/Vue handle that better)
- Complex routing or SSR needs (Next.js / Nuxt handle that)
- When youβre already committed to a specific framework ecosystem
π Can You Use Web Components Withβ¦
| Platform / Framework | Support | Notes |
|---|---|---|
| Vanilla HTML/CSS/JS | β Native | No setup β just import the component |
| WordPress | β Yes | Use <script type="module"> in block/theme β great for custom elements |
| Webflow | β Yes | Add via βEmbedβ blocks β load JS via CDN or custom code |
| Vue 3 | β Yes | Works as native elements; just ensure props/events are handled properly |
| React | β οΈ Partial | Works, but React treats Web Components differently (props/events may need manual wiring) |
| Angular | β Excellent | Built-in support for custom elements (Angular Elements use same spec) |
π§ Example Integration in Each Environment
πΉ Vanilla HTML
<script type="module" src="./todo-list.js"></script>
πΉ React
function App() {
return <todo-list></todo-list>;
}
If you need to pass props/events, use ref and addEventListener
πΉ Vue 3
<script setup>
import './todo-list.js';
</script>
πΉ WordPress
- You can enqueue your component like this in functions.php:
wp_enqueue_script('todo-list', get_template_directory_uri() . '/components/todo-list.js', [], null, true);
Then use:
<todo-list></todo-list>
π Summary
| Category | Details |
|---|---|
| Core Tech | Custom Elements, Shadow DOM, Templates, ES Modules |
| Main Benefit | Reusable UI across all frameworks |
| Best For | Widgets, design systems, multi-framework projects |
| Limitations | Less built-in state/reacΒtivity, limited SSR |
| Compatibility | Works with Vue, React, Angular, WordPress, Webflow, and vanilla HTML |
βοΈ Web Components vs React vs Vue (and Angular)
| π§© Category | π Web Components | βοΈ React | π’ Vue 3 | π °οΈ Angular |
|---|---|---|---|---|
| Core Concept | Native browser APIs to create custom HTML elements | Virtual DOM with declarative UI via JSX | Template-based reactive UI | Full-featured framework with TypeScript and DI |
| Language / Syntax | Plain JS, HTML, and CSS | JavaScript / TypeScript with JSX | HTML templates + reactive composition API | TypeScript |
| Performance | ποΈ Native, minimal overhead | β‘ Good, but uses virtual DOM diffing | β‘ Excellent with optimized reactivity | β‘ Great but heavier runtime |
| Bundle Size | πͺΆ Very small (no framework runtime) | βοΈ Medium (~30β50 KB) | βοΈ Medium (~30 KB) | π§± Large (~150 KB+) |
| Learning Curve | π‘ Moderate β must learn Shadow DOM, lifecycle, events | π΄ Moderate-High β JSX, hooks, state management | π’ Easy-Moderate β templates are intuitive | π΄ High β many concepts (modules, DI, RxJS) |
| Reactivity / State Management | β Manual (use JS classes or libraries like Lit, Signals, or RxJS) | β Built-in with hooks & state | β Built-in reactivity (Composition API) | β Built-in (RxJS, services) |
| DOM Handling | β Real DOM | βοΈ Virtual DOM | βοΈ Virtual DOM + fine-grained reactivity | βοΈ Virtual DOM |
| CSS Encapsulation | β Native Shadow DOM scoping | β Scoped via CSS-in-JS or modules | β Scoped styles in SFCs | β View encapsulation |
| Data Binding | βοΈ Manual (attributes/events) | β One-way | β
Two-way (v-model) | β
Two-way ([(ngModel)]) |
| SSR (Server-Side Rendering) | β Not natively supported | β Next.js / frameworks | β Nuxt.js | β Angular Universal |
| Tooling / Dev Ecosystem | π‘ Limited but growing (Stencil, Lit, Hybrids) | π’ Mature (React DevTools, CRA, Next.js) | π’ Mature (Vue DevTools, Vite, Nuxt) | π’ Mature (CLI, schematics) |
| Browser Support | β Native in all modern browsers (polyfill for old IE) | β All modern browsers | β All modern browsers | β All modern browsers |
| Framework Independence | π₯ Yes β pure web standard | β No | β No | β No |
| Integration in CMS (WordPress/Webflow) | β Very easy (native HTML tags) | β οΈ Needs bundling/transpilation | β οΈ Needs build setup | β οΈ Needs full app integration |
| Best Use Case | Reusable widgets, design systems, micro frontends | Full SPA, UI with dynamic data | Full SPA or progressive enhancements | Enterprise-scale apps |
| Community & Ecosystem | π‘ Smaller but W3C-standardized | π’ Massive | π’ Large and friendly | π’ Enterprise-focused |
| Learning Resources | π‘ Medium (fewer tutorials) | π’ Huge community | π’ Very large | π’ Extensive docs |
| Future Stability | π’ High β built into browsers | π’ High β constantly maintained | π’ High β official support | π’ High β Google-maintained |
π§ Quick Takeaways
β Use Web Components When:
- You need reusable, framework-independent UI (e.g., share across React + Vue + Angular apps).
- Youβre building widgets, dashboards, or CMS embeds (like
<user-chat>or<price-widget>). - You prefer no build step and want to ship pure HTML/JS/CSS.
βοΈ Use React When:
- You want large community support, rich ecosystem, and SSR (Next.js).
- You need fine-grained control over state and data flow.
- Youβre building a modern SPA or dashboard.
π’ Use Vue 3 When:
- You like template-based syntax and reactive state out of the box.
- You want a balance between simplicity and power.
- You prefer cleaner code and great DX (Developer Experience).
π °οΈ Use Angular When:
- Youβre building large, enterprise-scale applications.
- You need strict typing, dependency injection, and official structure.
- You prefer a complete solution (routing, forms, HTTP, testing).
π Hybrid Strategy (Best of All Worlds)
- Many companies now mix Web Components with frameworks:
- Design Systems: build UI atoms (e.g.,
<ui-button>,<ui-modal>) as Web Components β use in React/Vue/Angular apps. - CMS Widgets: embed reusable Web Components into WordPress or Webflow sites.
- Micro-frontends: teams use different frameworks but share the same Web Components.
Mini Design System
letβs build a mini design system using Web Components that works everywhere (React, Vue, Angular, WordPress, Webflow, or plain HTML).
Weβll create 3 reusable UI elements:
<ui-button>β styled button<ui-card>β content card<ui-modal>β modal dialog
All built with native Web Components, no frameworks or build tools.
π§± 1οΈβ£ Base Setup β ui-button.js
class UIButton extends HTMLElement {
static get observedAttributes() {
return ['variant'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
button {
font-family: system-ui, sans-serif;
font-size: 1rem;
padding: 8px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease;
}
button.primary {
background: #007bff;
color: white;
}
button.primary:hover {
background: #0056b3;
}
button.outline {
border: 2px solid #007bff;
color: #007bff;
background: transparent;
}
button.outline:hover {
background: #e8f0ff;
}
</style>
<button class="primary"><slot></slot></button>
`;
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'variant') {
const btn = this.shadowRoot.querySelector('button');
btn.className = newValue || 'primary';
}
}
}
customElements.define('ui-button', UIButton);
β Use it:
<ui-button>Primary</ui-button>
<ui-button variant="outline">Outline</ui-button>
π§© 2οΈβ£ ui-card.js
class UICard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border-radius: 12px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
background: white;
padding: 16px;
font-family: system-ui, sans-serif;
}
::slotted(h3) {
margin-top: 0;
}
::slotted(p) {
color: #555;
}
</style>
<slot></slot>
`;
}
}
customElements.define('ui-card', UICard);
β Use it:
<ui-card>
<h3>Title</h3>
<p>This is a reusable card.</p>
</ui-card>
πͺ 3οΈβ£ ui-modal.js
class UIModal extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.modal {
background: white;
border-radius: 12px;
padding: 20px;
max-width: 400px;
width: 90%;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { transform: translateY(-10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
button {
background: #007bff;
color: white;
border: none;
border-radius: 6px;
padding: 6px 12px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
<div class="modal">
<slot></slot>
<div style="text-align:right; margin-top:1em;">
<button id="closeBtn">Close</button>
</div>
</div>
`;
}
connectedCallback() {
this.shadowRoot.querySelector('#closeBtn').addEventListener('click', () => this.close());
}
open() {
this.style.display = 'flex';
}
close() {
this.style.display = 'none';
}
}
customElements.define('ui-modal', UIModal);
β Use it:
<ui-button id="openModal">Open Modal</ui-button>
<ui-modal id="myModal">
<h3>Hello!</h3>
<p>This is a reusable modal component.</p>
</ui-modal>
<script type="module">
import './ui-button.js';
import './ui-modal.js';
import './ui-card.js';
const modal = document.getElementById('myModal');
document.getElementById('openModal').addEventListener('click', () => modal.open());
</script>
βοΈ File Structure Example
/components/
ui-button.js
ui-card.js
ui-modal.js
index.html
π‘ Integration Examples
β Vanilla HTML
<script type="module" src="./components/ui-button.js"></script>
<ui-button>Click Me</ui-button>
β Vue 3
<template>
<ui-button>Click Me</ui-button>
<ui-card><h3>Hello Vue</h3><p>Using Web Components!</p></ui-card>
</template>
<script setup>
import '../components/ui-button.js';
import '../components/ui-card.js';
</script>
β React
import '../components/ui-button.js';
import '../components/ui-modal.js';
export default function App() {
return (
<>
<ui-button id="openModal">Open</ui-button>
<ui-modal id="myModal">
<h3>Hello React</h3>
</ui-modal>
</>
);
}
For events in React, use ref and addEventListener (React doesnβt natively handle custom events).
π§ Why This Is Powerful
| Benefit | Description |
|---|---|
| π Reusable | Works across React, Vue, Angular, Webflow, and WordPress |
| π§± Encapsulated | CSS and DOM scoped via Shadow DOM |
| π Fast | No framework overhead |
| π§© Composable | Components can use each other easily |
| π Deploy Anywhere | Drop in as simple <script type="module"> |
Conclusion
-
Web Components are the future of reusable, framework-independent UI.
-
They bring encapsulation, reusability, and interoperability β all powered by native browser APIs.
-
Whether youβre building a design system, widget, or micro frontend, Web Components help you write once and use anywhere.
-
β‘ In short: Write once, use everywhere.
-
Build UI thatβs truly universal β no frameworks required.
Continue Reading