// 1. Basic Custom Element
class MyButton extends HTMLElement {
constructor() {
super();
// Attach shadow DOM
const shadow = this.attachShadow({ mode: 'open' });
// Create elements
const button = document.createElement('button');
button.textContent = this.getAttribute('text') || 'Click me';
// Add styles
const style = document.createElement('style');
style.textContent = `
button {
padding: 10px 20px;
background: #4285f4;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #357abd;
}
`;
shadow.appendChild(style);
shadow.appendChild(button);
button.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('myclick', {
bubbles: true,
composed: true,
detail: { message: 'Button clicked!' }
}));
});
}
}
// Register the custom element
customElements.define('my-button', MyButton);
// Usage: <my-button text="Submit"></my-button>
// 2. Component with Template and Slot
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// Create template
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: block;
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
max-width: 300px;
}
.avatar {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
}
.name {
font-size: 1.25rem;
font-weight: bold;
margin: 0.5rem 0;
}
::slotted(p) {
color: #666;
}
</style>
<div class="card">
<img class="avatar" src="" alt="Avatar">
<h3 class="name"></h3>
<slot name="bio"></slot>
<slot></slot>
</div>
`;
shadow.appendChild(template.content.cloneNode(true));
}
// Lifecycle callbacks
connectedCallback() {
console.log('Component added to page');
this.render();
}
disconnectedCallback() {
console.log('Component removed from page');
}
adoptedCallback() {
console.log('Component moved to new page');
}
// Observed attributes
static get observedAttributes() {
return ['name', 'avatar'];
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
this.render();
}
render() {
const name = this.getAttribute('name') || 'Anonymous';
const avatar = this.getAttribute('avatar') || '/default-avatar.png';
const nameEl = this.shadowRoot.querySelector('.name');
const avatarEl = this.shadowRoot.querySelector('.avatar');
if (nameEl) nameEl.textContent = name;
if (avatarEl) avatarEl.src = avatar;
}
}
customElements.define('user-card', UserCard);
/* Usage:
<user-card name="Alex Chen" avatar="/alex.jpg">
<p slot="bio">Front-end developer</p>
<button>Follow</button>
</user-card>
*/
// 3. Advanced component with properties and methods
class TodoList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._todos = [];
}
connectedCallback() {
this.render();
this.attachEventListeners();
}
// Public API
addTodo(text) {
this._todos.push({ id: Date.now(), text, completed: false });
this.render();
}
removeTodo(id) {
this._todos = this._todos.filter(todo => todo.id !== id);
this.render();
}
toggleTodo(id) {
const todo = this._todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.render();
}
}
// Getters/setters
get todos() {
return this._todos;
}
set todos(value) {
this._todos = value;
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
ul { list-style: none; padding: 0; }
li {
padding: 0.5rem;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
gap: 0.5rem;
}
.completed { text-decoration: line-through; opacity: 0.6; }
button { padding: 0.25rem 0.5rem; }
</style>
<div>
<input type="text" id="new-todo" placeholder="Add todo">
<button id="add-btn">Add</button>
</div>
<ul>
${this._todos.map(todo => `
<li>
<input
type="checkbox"
${todo.completed ? 'checked' : ''}
data-id="${todo.id}"
>
<span class="${todo.completed ? 'completed' : ''}">
${todo.text}
</span>
<button data-id="${todo.id}" class="delete-btn">Delete</button>
</li>
`).join('')}
</ul>
`;
this.attachEventListeners();
}
attachEventListeners() {
const addBtn = this.shadowRoot.querySelector('#add-btn');
const input = this.shadowRoot.querySelector('#new-todo');
addBtn?.addEventListener('click', () => {
if (input.value.trim()) {
this.addTodo(input.value.trim());
input.value = '';
}
});
this.shadowRoot.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
this.toggleTodo(Number(e.target.dataset.id));
});
});
this.shadowRoot.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.removeTodo(Number(e.target.dataset.id));
});
});
}
}
customElements.define('todo-list', TodoList);
// 4. Extending built-in elements
class FancyButton extends HTMLButtonElement {
connectedCallback() {
this.classList.add('fancy');
this.addEventListener('click', this.handleClick);
}
handleClick() {
this.classList.add('clicked');
setTimeout(() => this.classList.remove('clicked'), 300);
}
}
customElements.define('fancy-button', FancyButton, { extends: 'button' });
// Usage: <button is="fancy-button">Click me</button>
Web Components create reusable custom elements with encapsulated functionality. I use Custom Elements API to define new HTML tags with customElements.define(). Shadow DOM provides style and markup encapsulation preventing CSS leakage. HTML Templates with <template> and <slot> enable flexible component structures. The attachShadow() method creates shadow root for isolated DOM. Lifecycle callbacks like connectedCallback() and disconnectedCallback() manage component lifecycle. Attributes and properties enable component configuration. Web Components work with any framework or vanilla JavaScript. They provide true component reusability across projects and frameworks.