Web Components Cross-Framework Reuse: Custom Elements / Shadow DOM
Introduction
Should a frontend architect understand Web Components? My answer is yes — not to use Lit / Stencil, but to build the engineering judgment of “the core abstraction layer of cross-framework component libraries”. Three reasons:
- The 8 cross-Vue/React universal components on resume is scarce experience. The core of cross-framework component libraries is Web Components + framework wrapper, understanding this lets you design framework-independent components.
- Web Components is a browser standard. Unlike React-only React, Custom Elements run in all frameworks — Vue / React / Angular / Solid / Svelte can all consume.
- The next 5 years’ cross-platform component library trend. Lit 3.0 / Lit SSR / Web Components + Signals / cross-framework component library design, all underneath is Web Components.
This is post #20 of the Frontend Architecture Cultivation Path series. We skip Lit’s specific API (no LitElement inheritance details), use architecture diagrams + design philosophy + real cases to make Web Components’ three core technologies + 8 cross-framework components design concrete. The next post deep-dives into front-end monitoring.
1. Web Components Three Core Technologies
Three core technologies:
- Custom Elements:
customElements.define('my-tag', ...)registers custom HTML tags - Shadow DOM: component’s internal DOM / styles / behavior fully isolated, doesn’t affect outside
- HTML Templates:
<template>+<slot>for reusable templates + content distribution
2. Custom Elements: Define Your Own HTML Tags
// smart-table.ts — a Web Component
class SmartTable extends HTMLElement {
static get observedAttributes() {
return ['columns', 'data-source', 'page-size'];
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const columns = JSON.parse(this.getAttribute('columns') || '[]');
const dataSource = this.getAttribute('data-source');
// render table
this.shadowRoot.innerHTML = `
<table>
<thead>
${columns.map(c => `<th>${c.label}</th>`).join('')}
</thead>
<tbody>
<slot></slot>
</tbody>
</table>
`;
}
}
customElements.define('smart-table', SmartTable);
Usage (any framework):
<!-- Pure HTML -->
<smart-table columns="[{"key":"name","label":"Name"}]" data-source="/api/users">
<tr><td>Alice</td></tr>
</smart-table>
<!-- Vue 3 -->
<smart-table :columns="cols" :data-source="apiUrl">
<tr v-for="row in data" :key="row.id"><td>{{ row.name }}</td></tr>
</smart-table>
<!-- React -->
<smartTable columns={JSON.stringify(cols)} dataSource="/api/users">
{data.map(row => <tr key={row.id}><td>{row.name}</td></tr>)}
</smartTable>
Advantages: one component, usable in Vue / React / Angular / Solid.
3. Shadow DOM: Component Style Isolation
class SmartTable extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }); // 0. attach Shadow DOM
}
connectedCallback() {
// 1. styles only affect inside Shadow DOM
this.shadowRoot.innerHTML = `
<style>
.table { border-collapse: collapse; }
th { background: #f0f0f0; padding: 8px; }
</style>
<table class="table">...</table>
`;
}
}
Three benefits of Shadow DOM:
- Style isolation: component’s internal
.buttonwon’t conflict with external.button - DOM isolation: component’s internal DOM doesn’t leak to global queries
- Event redirection: click events inside component can
composed: trueto bubble to outside
4. HTML Templates + Slot: Content Distribution
<template id="smart-table-tpl">
<style>.table { border-collapse: collapse; }</style>
<div class="container">
<slot name="header"></slot> <!-- named slot -->
<table class="table">
<slot></slot> <!-- default slot -->
</table>
<slot name="footer"></slot>
</div>
</template>
Consumer passes content:
<smart-table>
<h1 slot="header">User List</h1> <!-- insert header slot -->
<tr><td>Alice</td></tr> <!-- insert default slot -->
<button slot="footer">Export</button> <!-- insert footer slot -->
</smart-table>
Slot is Web Components version of React children / Vue slot, cross-framework consistent.
5. 8 Cross-Vue/React Universal Components Design Philosophy
The resume line “8 cross-Vue/React universal components” — the core architecture is Web Components abstraction + framework wrapper:
graph TD
A[Web Components core<br/>SmartTable / SmartForm / SmartDatePicker] --> B[Vue 3 wrapper<br/>v-model + props binding]
A --> C[React wrapper<br/>ref + custom hooks]
A --> D[Angular wrapper<br/>CUSTOM_ELEMENTS_SCHEMA]
Architect’s mantra:
- Web Components write core logic (state management, DOM operations, event distribution)
- Framework wrappers only do adaptation (props ↔ attribute mapping, events ↔ callback mapping)
- Business code uses framework wrappers (DX + type inference)
6. Lit / Stencil / Self-Implementation Selection
| Solution | Pros | Cons |
|---|---|---|
| Lit | Modern, TS-friendly, active ecosystem | Learning curve |
| Stencil | Compile-time optimization, multi-framework output | Complex config |
| Self-implement HTMLElement | Zero deps, flexible | Lots of work |
| Vue / React wrap Web Components | Reuse existing components | Still one per framework |
Architect’s decision:
- New project, efficiency focused → Lit
- Existing Vue / React component library → Stencil compile output
- Extreme performance / zero deps → Native HTMLElement
7. Production Practice: FinUI Cross-Framework Component Library
graph LR
A[Requirement<br/>FinUI component library<br/>Vue 3 + React dual version] --> B[Core implementation<br/>Lit writes Web Component]
B --> C[Vue 3 wrapper<br/>finui-vue / v-model + TS types]
B --> D[React wrapper<br/>finui-react / forwardRef + types]
C --> E[SF Express ERP<br/>finui.example.com]
D --> F[Rouyu admin<br/>rouyu.example.com]
Production data:
- 8 cross-framework universal components (SmartTable / SmartForm / SmartDatePicker / SmartSelect / SmartTree / SmartUpload / SmartModal / SmartEditor)
- Vue 3 + React dual version sync (change core once, both auto-sync)
- 60% maintenance cost saved (no need to write one for Vue, one for React)
8. Pitfall Reminders (Senior Architects Please Read Carefully)
- Don’t let events inside Shadow DOM not bubble. Default events inside Shadow DOM don’t bubble to outside — add
composed: true. - Don’t use global CSS to select inside Shadow DOM. Shadow DOM isolation is this — can’t use
.my-component .buttonto cross boundaries. Expose CSS Variables to allow changes (--button-bg). - Don’t let framework wrappers directly use attributes.
nullgets stringified — must usegetAttribute()to check null. - Don’t use framework state management in Web Components. Web Components are framework-agnostic — use native
observedAttributes, don’t mix Redux / Pinia / Zustand. - Don’t ignore SSR. Web Components default to CSR, SSR needs lit-ssr / similar solutions. SEO scenarios must configure.
Summary
Six key facts that thread Web Components together:
- Three core technologies: Custom Elements + Shadow DOM + HTML Templates, browser-native, cross-framework.
- Custom Elements: custom HTML tags, consumable by Vue/React/Angular/Solid.
- Shadow DOM: style and structure isolation, zero CSS conflicts.
- Slot: cross-framework consistent content distribution mechanism.
- Cross-framework component library: Web Components core + framework wrapper, 8 universal components save 60% maintenance.
- Selection decision: new project Lit / existing library Stencil / zero-dep native HTMLElement.
Next post: Front-end monitoring system — error / performance / behavior monitoring, SF Express + Rouyu two monitoring practice summaries.
5 Key Interview Question Tracks
This section maps one-to-one with the article. Q1 ships with a complete answer as a model; the other four are for you to think through — each one is followed by an AI assistant button for a one-click detailed answer.
Q1 (Answer): What are Web Components’ three core technologies? Why are Web Components the core abstraction for cross-framework component libraries?
A: Three core technologies: ① Custom Elements —
customElements.defineregisters custom HTML tags, usable in Vue / React / Angular / Solid; ② Shadow DOM — component internal DOM / style isolation, won’t conflict with external CSS; ③ HTML Templates + Slot — cross-framework consistent content distribution mechanism. Why it’s cross-framework core abstraction: Web Components is a W3C standard, natively supported by all modern browsers, not bound to any framework. The 8 cross-Vue/React universal components on resume’s core architecture is: Web Components writes core logic + Vue/React wrapper does adaptation — change core once, two frameworks sync, 60% maintenance cost saved.Q2 (Think): How does Shadow DOM’s style isolation work? How to allow external modification of certain styles inside a Web Component?
A: Shadow DOM implements style isolation through CSS isolated scopes — internal CSS only affects inside Shadow DOM, doesn’t leak outside. For external modifications use CSS Variables — component defines
var(--button-bg), externalstyle="--button-bg: red"can change. Practice: a cross-framework component library exposes all colors / spacing / fonts as CSS Variables, business can theme customize without breaking encapsulation.Q3 (Think): What are Custom Elements’ lifecycles? How to listen to attribute changes?
A: Custom Elements four lifecycles: ①
constructor()— called on creation, mustsuper()and cannot return; ②connectedCallback()— called when element inserted into DOM, called only once; ③disconnectedCallback()— called when element removed from DOM, cleanup side effects; ④attributeChangedCallback(name, oldValue, newValue)— called when attribute changes. Listen to attribute changes: definestatic get observedAttributes()on class returning attribute names array, only these attributes’ changes triggerattributeChangedCallback. Production pitfall:attributeChangedCallbackalso fires beforeconnectedCallback, must ensurethis.shadowRootis already initialized at that time.Q4 (Think): How does Web Components’ event bubbling differ from React / Vue’s event systems?
A: Web Components event bubbling is affected by Shadow DOM boundary — events inside Shadow DOM don’t bubble to outside by default. Solutions: ① Event
composed: truelets event penetrate Shadow DOM boundary; ② Eventbubbles: truelets event bubble. Practice: click button inside component,composed: truelets outside listen. Production case: SmartTable internal row click event,composed: truebubbles to Vue / React wrapper, business code uses@row-click/onRowClickto listen, completely framework-agnostic.Q5 (Think): In your FinUI cross-framework component library, how did you design 8 universal components? How exactly does Web Components core + framework wrapper division of labor work?
A: 8 universal components = SmartTable / SmartForm / SmartDatePicker / SmartSelect / SmartTree / SmartUpload / SmartModal / SmartEditor. Division of labor: Web Components writes core logic — Shadow DOM encapsulation + Custom Elements registration + observedAttributes props listening + slot content distribution; framework wrappers only do adaptation — Vue uses
defineComponent + v-model/ React usesforwardRef + useImperativeHandle. Practice data: change core once, Vue 3 + React dual version sync, save 60% maintenance cost — Vue team usesfinui-vue, React team usesfinui-react, component library team only maintains one Lit core.
💡 Each question has an AI assistant button — one click gets you a detailed answer.
References
- MDN: Web Components — Web Components official reference
- Lit Documentation — Lit official documentation
- Custom Elements (web.dev) — Custom Elements practical guide
- Shadow DOM (web.dev) — Shadow DOM practical guide
- Web Components Best Practices — Web Components best practices
- Open Web Components — open-source Web Components community