💡 Running Code Locally
Custom Elements and Shadow DOM work in all modern browsers. To test these components:
Master the foundations of component architecture without frameworks. Learn Custom Elements, Shadow DOM, slots, state management, and patterns used by React, Vue, and Svelte.
A reusable component is a self-contained piece of UI with its own markup, styles, logic, and state. It can be instantiated multiple times, accepts inputs (props), emits events, and manages its own lifecycle.
💡 Why Pure JS? Zero dependencies, faster than framework components, works anywhere, teaches real DOM mastery, and you can port logic to React/Vue easily.
The simplest pattern for creating reusable components. A function returns an object with the DOM element and a public API for external control.
Create reusable components using factory functions
// Factory Function Pattern - Simple Counter Component
function createCounter({ start = 0 }) {
// Internal state
let count = start;
// Create DOM structure
const root = document.createElement("div");
root.className = "counter";
root.innerHTML = `
<button class="dec">-</button>
<span class="value">${count}</span>
<button class="inc">+</button>
`;
const valueEl = root.querySelector(".value");
// Update function - re-renders when state changes
function update() {
...For HTML-driven components, use the <template> element. Templates are not rendered but can be cloned efficiently, making this pattern very fast.
Use template elements for HTML-driven components
// Template Clone Pattern - HTML-driven components
// HTML template (would be in your HTML file):
// <template id="card-template">
// <div class="card">
// <h3 class="title"></h3>
// <p class="content"></p>
// </div>
// </template>
function createCard({ title, content, onClick }) {
// Clone the template
const template = document.querySelector("#card-template");
const root = template.content.cloneNode(true);
// Fill in data
root.querySelector(".title").textContent = title;
...Components emit custom events to communicate with parent code. This keeps components loosely coupled and reusable across different contexts.
Components emit custom events to communicate with parents
// Component Communication with Custom Events
function createNotificationBanner() {
const root = document.createElement("div");
root.className = "notification-banner";
root.innerHTML = `
<span class="message"></span>
<button class="close">×</button>
`;
const messageEl = root.querySelector(".message");
const closeBtn = root.querySelector(".close");
// Emit custom event when closed
closeBtn.addEventListener("click", () => {
// Dispatch a custom event that parents can
...Shadow DOM creates a private DOM tree inside your element. Styles are completely isolated — no leakage to or from global CSS. This is how browsers implement <video>, <input>, and <select>.
Create private DOM trees with isolated styles
// Shadow DOM - Full UI Encapsulation
class MyCard extends HTMLElement {
constructor() {
super();
// Attach shadow DOM (private DOM tree)
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
/* These styles are COMPLETELY isolated */
.card {
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.card h3 {
margin: 0 0 8px 0;
...✅ Shadow DOM Benefits: Style encapsulation, no naming conflicts, framework-level component scoping, and markup isolation.
Custom Elements are the browser's native component system. They have lifecycle callbacks similar to React, can observe attribute changes, and are fully reusable.
Browser-native components with lifecycle callbacks
// Full Custom Element with Lifecycle
class ToggleSwitch extends HTMLElement {
// Declare which attributes to watch
static get observedAttributes() {
return ["checked", "disabled"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
}
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
.switch {
wid
...Slots let users inject content into specific areas of your component — similar to React's children prop but with named targets.
Inject content into specific areas of components
// Named Slots for Flexible Component Composition
class ModalDialog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
:host {
display: none;
}
:host([open]) {
display: block;
}
.overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify
...Generate multiple components from a single factory function. This pattern lets you create consistent UI libraries with minimal code duplication.
Generate multiple components from a single factory
// Component Factory - Generate Components from Config
function createComponent({ name, template, style, methods = {} }) {
class GeneratedComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>${style}</style>
${template}
`;
// Call init if provided
if (methods.init) {
methods.init.call(this);
}
}
connectedCallback() {
methods.
...For shared state across components (theme, user session, notifications), implement a simple store with subscribers — the same pattern used by Redux and Vuex.
Implement a simple store with subscribers like Redux
// Global State Store (Redux/Vuex Pattern)
const Store = {
state: {
theme: "light",
user: null,
notifications: []
},
listeners: new Set(),
// Get current state
getState() {
return { ...this.state };
},
// Update state
set(key, value) {
this.state[key] = value;
this.notify();
},
// Subscribe to changes
subscribe(callback) {
this.listeners.add(callback);
// Return unsubscribe function
return () => this.listeners.delete(callback);
},
...An event bus allows any component to emit or listen to events without direct references. This enables loose coupling between unrelated components.
Enable loose coupling between unrelated components
// Event Bus for Cross-Component Communication
const EventBus = {
events: new Map(),
// Subscribe to an event
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, new Set());
}
this.events.get(event).add(callback);
// Return unsubscribe function
return () => this.events.get(event).delete(callback);
},
// Emit an event
emit(event, data) {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(fn =>
...Components that load data before rendering are essential for real applications. Handle loading states, errors, and dynamic attribute changes.
Components that load data before rendering
// Async Components - Load Data Before Rendering
class UserProfile extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
:host { display: block; }
.loading {
padding: 20px;
text-align: center;
color: #6b7280;
}
.error {
padding: 20px;
color: #ef4444;
background: #fef2f2;
border-radius: 8px;
}
...Professional components must be accessible. Implement ARIA roles, keyboard navigation, focus management, and screen reader support.
ARIA roles, keyboard navigation, and screen reader support
// Accessible Custom Components
class AccessibleTabs extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.selectedIndex = 0;
this.shadowRoot.innerHTML = `
<style>
.tablist {
display: flex;
border-bottom: 2px solid #e5e7eb;
}
.tab {
padding: 12px 20px;
background: none;
border: none;
cursor: pointer;
font-size: 14px;
color: #6b7280;
...💡 Accessibility Checklist: ARIA roles, keyboard navigation (Arrow keys, Home, End), focus states, aria-selected/aria-hidden, proper tabindex, and screen reader labels.
For large-scale applications, optimize with template caching, batched updates, object pooling, lazy initialization, and event delegation.
Template caching, batched updates, and object pooling
// High-Performance Component Patterns
// 1. Template Caching - Clone instead of recreate
class OptimizedList extends HTMLElement {
static template = null;
constructor() {
super();
this.attachShadow({ mode: "open" });
// Cache template once
if (!OptimizedList.template) {
const tmpl = document.createElement("template");
tmpl.innerHTML = `
<style>
.item { padding: 12px; border-bottom: 1px solid #eee; }
</style>
<div class="item">
...Sign up for free to track which lessons you've completed and get learning reminders.