Web Components Architecture
This lesson explains how Web Components fit into modern frontend architecture, how custom elements, Shadow DOM, templates, and events work together, and how to structure reusable component libraries for production.
What Is Web Components Architecture?
Web Components architecture describes how browser-native standards
combine to create reusable, encapsulated UI building blocks. A
well-designed component library separates public APIs, internal
implementation, styling, and communication so teams can share UI
safely across applications.
At a high level, architecture is built from three standards:
Custom Elements for behavior,
Shadow DOM for encapsulation, and
HTML Templates for reusable markup.
| Layer | Role |
| Custom Elements | Define new HTML tags and lifecycle behavior |
| Shadow DOM | Encapsulate internal DOM and CSS |
| HTML Templates | Store reusable markup safely |
| Slots | Compose external content into components |
| Custom Events | Send data outward to parent apps |
| Public API | Attributes, properties, events, and styling hooks |
Basic Component Architecture Example
class AppButton extends HTMLElement {
static get observedAttributes() {
return ["variant", "disabled"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const variant =
this.getAttribute("variant") || "primary";
const disabled =
this.hasAttribute("disabled");
this.shadowRoot.innerHTML = `
<style>
button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
}
.primary { background: #2563eb; color: white; }
.secondary { background: #e5e7eb; color: #111827; }
</style>
<button class="${variant}" ?disabled="${disabled}">
<slot></slot>
</button>
`;
}
}
customElements.define("app-button", AppButton);
This example shows a common architecture pattern: lifecycle hooks
manage rendering, Shadow DOM encapsulates styles, attributes define
the public API, and slots allow flexible content.
Component Anatomy
| Part | Description | Example |
| Host Element | The custom element in the light DOM. | <app-button> |
| Shadow Root | Encapsulated internal tree. | this.shadowRoot |
| Internal Template | Private markup and styles. | render() output |
| Slots | Projection points for child content. | <slot name="icon"> |
| Public API | What consumers can configure. | Attributes and properties |
| Events | Output channel to parent apps. | button-click |
Lifecycle Architecture
class DataPanel extends HTMLElement {
connectedCallback() {
console.log("Added to DOM");
this.loadData();
}
disconnectedCallback() {
console.log("Removed from DOM");
this.cleanup();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.updateFromAttribute(name, newValue);
}
}
adoptedCallback() {
console.log("Moved to another document");
}
}
Lifecycle callbacks form the backbone of component architecture.
They tell you when to render, fetch data, attach listeners, and
release resources.
| Callback | When It Runs | Typical Use |
connectedCallback | Element added to DOM | Initial render, event listeners, data fetch |
disconnectedCallback | Element removed from DOM | Cleanup timers, observers, listeners |
attributeChangedCallback | Observed attribute changes | Sync UI with attribute updates |
adoptedCallback | Element moved to new document | Reinitialize document-specific behavior |
Communication Architecture
Web Components communicate through a predictable set of channels.
Good architecture keeps data flow clear and avoids tight coupling
between parent apps and internal implementation details.
// Input: attributes and properties
const card = document.querySelector("user-card");
card.setAttribute("role", "admin");
card.userId = 42;
// Output: custom events
card.addEventListener("profile-selected", (event) => {
console.log(event.detail);
});
// Composition: slots
// <user-card>
// <span slot="badge">Pro</span>
// </user-card>
| Direction | Mechanism | Best For |
| Parent → Component | Attributes and properties | Configuration and state input |
| Component → Parent | Custom events | User actions and state changes |
| Parent → Component | Slots | Flexible markup and content |
| External → Internal Styles | CSS custom properties and parts | Theme and design tokens |
Shadow DOM Architecture
class ModalDialog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host { display: block; }
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
}
::slotted(h2) {
margin-top: 0;
}
</style>
<div class="overlay" part="overlay">
<div class="dialog" part="dialog">
<slot name="title"></slot>
<slot></slot>
</div>
</div>
`;
}
}
Shadow DOM creates a boundary between public page markup and
private component internals. Use slots for composition,
::part() for limited styling hooks, and CSS custom
properties for theming.
Composition Over Inheritance
<app-card>
<h2 slot="title">Team Members</h2>
<user-row name="Alex" role="Admin"></user-row>
<user-row name="Sam" role="Editor"></user-row>
<app-button slot="footer" variant="primary">
Add Member
</app-button>
</app-card>
Web Components architecture favors composition. Small focused
elements such as <app-card>,
<user-row>, and <app-button>
combine into larger UI without deep inheritance trees.
Component Library Structure
ui-components/
├── src/
│ ├── button/
│ │ ├── app-button.js
│ │ └── app-button.test.js
│ ├── card/
│ │ ├── app-card.js
│ │ └── app-card.test.js
│ ├── tokens/
│ │ └── design-tokens.css
│ └── index.js
├── docs/
│ └── usage.md
└── package.json
Production architecture usually organizes one component per folder,
shared design tokens in a central location, a single registration
entry point, and documentation for attributes, events, and slots.
Framework Integration Architecture
// React wrapper pattern
function StatusBadge(props) {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
ref.current.status = props.status;
}
}, [props.status]);
return (
<status-badge ref={ref}>
{props.children}
</status-badge>
);
}
Framework apps often consume Web Components directly, but wrappers
can help synchronize properties, events, and SSR requirements when
needed.
Common Architecture Patterns
| Pattern | Description | Example |
| Presentational Components | Focus on UI rendering and styling. | <app-badge> |
| Container Components | Fetch data and pass it to child elements. | <user-list-panel> |
| Form Controls | Expose value, validation, and events. | <app-text-input> |
| Shell Components | Provide layout and slot-based regions. | <app-layout> |
| Design System Primitives | Low-level reusable building blocks. | <app-button> |
Open vs Closed Shadow DOM
| Mode | Access | When to Use |
open | element.shadowRoot is available | Most components, testing, tooling, theming |
closed | Shadow root is hidden from outside code | Strong encapsulation, third-party widgets |
When This Architecture Helps Most
- You are building a shared UI library for multiple teams.
- You need consistent APIs across framework boundaries.
- You want encapsulated styling without global CSS conflicts.
- You are designing a design system with primitives and composites.
- You need long-lived components that survive framework migrations.
- You are planning micro frontends with independently deployable UI.
Architecture Use Cases
- Company-wide design systems with buttons, inputs, and cards.
- Embeddable widgets distributed to external websites.
- Shared navigation and shell layouts across products.
- Framework-agnostic form controls with validation APIs.
- Micro frontend platforms with isolated UI packages.
- Documentation sites that demo live custom elements.
Architecture Best Practices
- Define a clear public API with attributes, properties, and events.
- Keep components small and composable instead of monolithic.
- Use Shadow DOM intentionally with a documented styling strategy.
- Separate rendering, state sync, and event handling cleanly.
- Document slots, CSS parts, and design tokens for consumers.
- Clean up listeners and observers in
disconnectedCallback. - Version and publish component libraries like any shared package.
Common Architecture Mistakes
- Putting business logic, fetching, and UI in one giant component.
- Exposing internal Shadow DOM details as the public API.
- Using attributes for complex objects instead of properties.
- Emitting too many unclear custom events.
- Skipping cleanup when components are removed from the DOM.
- Not planning theming with CSS custom properties.
- Building components without tests for lifecycle and API behavior.
Key Takeaways
- Web Components architecture combines Custom Elements, Shadow DOM, and Templates.
- Lifecycle callbacks control render timing, setup, and cleanup.
- Attributes, properties, slots, and events form the communication model.
- Composition beats inheritance for scalable component libraries.
- Good architecture makes shared UI reusable across apps and frameworks.
Pro Tip
Design each component around a single responsibility and a small
public API. A stable architecture with clear inputs, outputs, and
slots scales much better than one large custom element that tries
to do everything.