Component Crusaders
“A Journey Through Shadow DOM, Slots, and the Art of Reusable Components”
When I first started building my portfolio, I wanted something modern, reusable, and framework-agnostic. That’s when I discovered Web Components.
Web Components let you create custom HTML elements that encapsulate structure, styling, and behavior — all reusable across projects. Unlike frameworks, they’re native to the browser, lightweight, and framework-independent.
⚙️ Why I Chose Web Components
While frameworks like React or Vue offer reusable components and scoped CSS (via module.scss), Web Components offer true runtime encapsulation through Shadow DOM:
Encapsulation – Shadow DOM ensures styles and markup don’t leak in or out.
Reusability – Once built, components can be dropped into any project or page.
Framework-agnostic – They work anywhere, even alongside React, Vue, or vanilla JS.
Native performance – No extra runtime overhead; the browser handles them natively.
Tip: CSS Modules in React only scope class names at build time, but Shadow DOM isolates the entire DOM subtree at runtime.
🧩 Defining a Custom Element
A Custom Element is a new HTML tag you define, e.g., <portfolio-card>.
class PortfolioCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.card {
border: 1px solid #ccc;
padding: 1rem;
border-radius: 0.5rem;
transition: box-shadow 0.3s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
</style>
<div class="card">
<slot></slot>
</div>
`;
}
connectedCallback() {
console.log('PortfolioCard added to the DOM');
}
disconnectedCallback() {
console.log('PortfolioCard removed from the DOM');
}
adoptedCallback() {
console.log('PortfolioCard moved to a new document');
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
static get observedAttributes() {
return ['data-title']; // watch changes for this attribute
}
}
customElements.define('portfolio-card', PortfolioCard);
Lifecycle Callbacks Explained
connectedCallback()– Called when the element is added to the DOM.disconnectedCallback()– Called when the element is removed from the DOM.adoptedCallback()– Called when the element is moved to a new document.attributeChangedCallback(name, oldValue, newValue)– Called when an observed attribute changes.
These methods let you react to changes, similar to React hooks (useEffect) but at the native browser level.
🔄 Using Slots: Injecting Content
<portfolio-card>
<h2>My Awesome Project</h2>
<p>This project demonstrates Web Components in action!</p>
</portfolio-card>
The
<slot>inside<portfolio-card>receives<h2>and<p>from outside.Shadow DOM ensures the component’s styles encapsulate the content without affecting the rest of the page.
Named slots:
shadow.innerHTML = `
<div class="header"><slot name="header"></slot></div>
<div class="body"><slot name="body"></slot></div>
`;
<portfolio-card>
<span slot="header">Project Title</span>
<span slot="body">This is a description of the project.</span>
</portfolio-card>
🔍 Querying and Manipulating Shadow DOM
You can interact with elements inside Shadow DOM using the shadowRoot:
class PortfolioCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }).innerHTML = `
<div class="card">
<p class="content">Initial content</p>
<button id="btn">Click me</button>
</div>
`;
}
connectedCallback() {
const btn = this.shadowRoot.querySelector('#btn');
const content = this.shadowRoot.querySelector('.content');
btn.addEventListener('click', () => {
content.textContent = 'Content updated via Shadow DOM!';
});
}
}
customElements.define('portfolio-card', PortfolioCard);
✅ You can query, modify, and append elements inside Shadow DOM just like normal DOM, but outside styles or JS cannot affect it unless you explicitly expose it.
⚡ Benefits I Experienced
Cleaner Codebase – Each UI block is self-contained.
Reusable Across Projects – Drop components anywhere, no extra framework required.
Future-Proof – Works independently of frameworks.
Better Understanding of Browser – Deepens knowledge of native DOM, events, and rendering.
⚠️ Challenges with Web Components
Browser Support – Modern browsers support Web Components, older ones need polyfills.
- Solution: Use @webcomponents/webcomponentsjs for legacy support.
Styling Shadow DOM – Global styles don’t penetrate, and you can’t style deep elements outside.
- Solution: Use CSS variables for theming, or pass styles via
::partand::theme.
- Solution: Use CSS variables for theming, or pass styles via
State Management – Unlike React, you need to manually handle updates inside the component.
- Solution: Use observed attributes or internal JS state + lifecycle callbacks.
Tooling and Debugging – Dev tools and IDE support are less mature.
- Solution: Use Chrome/Firefox DevTools for inspecting Shadow DOM; consider small helper libraries if needed.
Communication Between Components – Passing events or data between multiple custom elements can be tricky.
- Solution: Use Custom Events or Shared State via global store.
🧠 Lessons Learned
Web Components excel in design systems, micro frontends, or static websites.
Shadow DOM gives true runtime isolation, beyond CSS Modules or framework scoping.
Slots make components dynamic and reusable.
Lifecycle callbacks allow native reactivity without any framework overhead.
💡 Takeaway
Web Components allow you to write encapsulated, reusable, and framework-independent components using native browser APIs.
By combining Custom Elements + Shadow DOM + Slots + Lifecycle Callbacks, you can build UI that’s maintainable, modular, and future-proof, even for personal projects like a portfolio.
