Web Components Complete Guide | Native Custom Elements
이 글의 핵심
Web Components are a suite of browser standards that let you create reusable custom elements with encapsulated functionality. They work everywhere without frameworks.
Introduction
Web Components are a set of web platform APIs that allow you to create custom, reusable HTML elements with encapsulated functionality. They’re built into the browser and don’t require any framework.
The Three Pillars
- Custom Elements - Define new HTML tags
- Shadow DOM - Encapsulate styles and markup
- HTML Templates - Reusable HTML chunks
Why Web Components?
Problem with frameworks:
// React component only works in React
function MyButton({ text }) {
return <button>{text}</button>;
}
// Vue component only works in Vue
export default {
template: '<button>{{ text }}</button>'
}
Web Components work everywhere:
// Works in React, Vue, Angular, or vanilla JS
class MyButton extends HTMLElement {
connectedCallback() {
this.innerHTML = `<button>${this.textContent}</button>`;
}
}
customElements.define('my-button', MyButton);
<!-- Use anywhere -->
<my-button>Click me</my-button>
1. Custom Elements
Basic Custom Element
class HelloWorld extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>Hello, World!</h1>';
}
}
customElements.define('hello-world', HelloWorld);
<hello-world></hello-world>
With Attributes
class UserCard extends HTMLElement {
connectedCallback() {
const name = this.getAttribute('name') || 'Guest';
const role = this.getAttribute('role') || 'User';
this.innerHTML = `
<div class="user-card">
<h2>${name}</h2>
<p>${role}</p>
</div>
`;
}
}
customElements.define('user-card', UserCard);
<user-card name="Alice" role="Developer"></user-card>
Lifecycle Callbacks
class MyElement extends HTMLElement {
// Called when element is created
constructor() {
super();
console.log('Constructor');
}
// Called when added to DOM
connectedCallback() {
console.log('Connected');
this.render();
}
// Called when removed from DOM
disconnectedCallback() {
console.log('Disconnected');
}
// Called when moved to new page
adoptedCallback() {
console.log('Adopted');
}
// Called when attribute changes
attributeChangedCallback(name, oldValue, newValue) {
console.log(`${name} changed from ${oldValue} to ${newValue}`);
this.render();
}
// Specify which attributes to observe
static get observedAttributes() {
return ['name', 'age'];
}
render() {
const name = this.getAttribute('name');
this.innerHTML = `<p>Hello, ${name}!</p>`;
}
}
customElements.define('my-element', MyElement);
2. Shadow DOM
Shadow DOM provides encapsulation - styles and markup hidden from the main document.
Basic Shadow DOM
class ShadowCard extends HTMLElement {
connectedCallback() {
// Attach shadow root
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
/* Styles scoped to shadow DOM */
.card {
border: 1px solid #ccc;
padding: 1rem;
border-radius: 8px;
}
h2 { color: blue; }
</style>
<div class="card">
<h2>Shadow Card</h2>
<p>This is encapsulated!</p>
</div>
`;
}
}
customElements.define('shadow-card', ShadowCard);
Benefits:
- Styles don’t leak out
- External styles don’t leak in
- DOM structure hidden
Slots (Content Projection)
class FancyButton extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
}
</style>
<button>
<slot></slot>
</button>
`;
}
}
customElements.define('fancy-button', FancyButton);
<fancy-button>Click Me!</fancy-button>
Named Slots
class UserProfile extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.profile {
display: flex;
gap: 1rem;
padding: 1rem;
border: 1px solid #ddd;
}
.avatar { width: 64px; height: 64px; }
</style>
<div class="profile">
<slot name="avatar"></slot>
<div>
<slot name="name"></slot>
<slot name="bio"></slot>
</div>
</div>
`;
}
}
customElements.define('user-profile', UserProfile);
<user-profile>
<img slot="avatar" src="avatar.jpg">
<h2 slot="name">Alice</h2>
<p slot="bio">Software Developer</p>
</user-profile>
3. HTML Templates
<template id="user-card-template">
<style>
.card {
border: 1px solid #ccc;
padding: 1rem;
margin: 0.5rem;
}
</style>
<div class="card">
<h3></h3>
<p></p>
</div>
</template>
<script>
class UserCard extends HTMLElement {
connectedCallback() {
const template = document.getElementById('user-card-template');
const clone = template.content.cloneNode(true);
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(clone);
shadow.querySelector('h3').textContent = this.getAttribute('name');
shadow.querySelector('p').textContent = this.getAttribute('role');
}
}
customElements.define('user-card', UserCard);
</script>
4. Properties and Methods
class Counter extends HTMLElement {
constructor() {
super();
this._count = 0;
this.attachShadow({ mode: 'open' });
}
// Getter/setter for count property
get count() {
return this._count;
}
set count(value) {
this._count = value;
this.render();
}
// Public method
increment() {
this.count++;
}
connectedCallback() {
this.render();
this.shadowRoot.querySelector('button').addEventListener('click', () => {
this.increment();
});
}
render() {
this.shadowRoot.innerHTML = `
<style>
button { padding: 8px 16px; }
</style>
<div>
<p>Count: ${this.count}</p>
<button>Increment</button>
</div>
`;
}
}
customElements.define('my-counter', Counter);
// Use from JavaScript
const counter = document.querySelector('my-counter');
counter.count = 10;
counter.increment();
5. Events
Dispatching Custom Events
class TodoItem extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.render();
this.shadowRoot.querySelector('button').addEventListener('click', () => {
// Dispatch custom event
this.dispatchEvent(new CustomEvent('todo-complete', {
detail: { id: this.getAttribute('id') },
bubbles: true,
composed: true // Cross shadow boundary
}));
});
}
render() {
this.shadowRoot.innerHTML = `
<div>
<span>${this.getAttribute('text')}</span>
<button>Complete</button>
</div>
`;
}
}
customElements.define('todo-item', TodoItem);
// Listen for custom event
document.addEventListener('todo-complete', (e) => {
console.log('Todo completed:', e.detail.id);
});
6. Real-World Example: Modal Dialog
class ModalDialog extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
// Close on backdrop click
this.shadowRoot.querySelector('.backdrop').addEventListener('click', () => {
this.close();
});
// Close on close button
this.shadowRoot.querySelector('.close').addEventListener('click', () => {
this.close();
});
}
open() {
this.style.display = 'block';
document.body.style.overflow = 'hidden';
}
close() {
this.style.display = 'none';
document.body.style.overflow = '';
this.dispatchEvent(new Event('modal-closed'));
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: none;
position: fixed;
inset: 0;
z-index: 1000;
}
.backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
.modal {
position: relative;
max-width: 500px;
margin: 5rem auto;
background: white;
border-radius: 8px;
padding: 2rem;
}
.close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
</style>
<div class="backdrop"></div>
<div class="modal">
<button class="close">×</button>
<slot></slot>
</div>
`;
}
}
customElements.define('modal-dialog', ModalDialog);
<button onclick="document.querySelector('modal-dialog').open()">
Open Modal
</button>
<modal-dialog>
<h2>Modal Title</h2>
<p>Modal content goes here...</p>
</modal-dialog>
7. Best Practices
1. Use Semantic Tag Names
// Good: descriptive, kebab-case
customElements.define('user-profile', UserProfile);
customElements.define('todo-list', TodoList);
// Bad: too generic
customElements.define('my-element', MyElement);
2. Always Use Shadow DOM for Encapsulation
// Good: styles encapsulated
class MyButton extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `<style>...</style><button>...</button>`;
}
}
// Bad: styles leak to global scope
class MyButton extends HTMLElement {
connectedCallback() {
this.innerHTML = `<style>...</style><button>...</button>`;
}
}
3. Clean Up Event Listeners
class MyElement extends HTMLElement {
connectedCallback() {
this.handleClick = () => console.log('Clicked');
this.addEventListener('click', this.handleClick);
}
disconnectedCallback() {
this.removeEventListener('click', this.handleClick);
}
}
8. Framework Integration
React
function App() {
return (
<div>
<user-card name="Alice" role="Developer"></user-card>
</div>
);
}
Vue
<template>
<user-card name="Alice" role="Developer"></user-card>
</template>
Angular
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
Summary
Web Components enable framework-agnostic reusable components:
- Custom Elements for new HTML tags
- Shadow DOM for style encapsulation
- HTML Templates for reusable markup
- Native browser APIs - no build tools required
- Works everywhere - any framework or vanilla JS
Key Takeaways:
- Use
customElements.define()to register - Shadow DOM for encapsulation
- Lifecycle callbacks for component logic
- Dispatch custom events for communication
- Compatible with all modern frameworks
Next Steps:
- Build with Lit
- Learn Vanilla JavaScript
- Try Stencil
Resources:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Complete Web Components guide for building reusable components. Learn Custom Elements, Shadow DOM, HTML Templates, and b…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- JavaScript DOM Manipulation | Control Web Pages Dynamically
- Lit Complete Guide | Fast Web Components Library
- Alpine.js Complete Guide | Lightweight JavaScript Framework
Keywords Covered in This Article (Related Search Terms)
This article covers Web Components, JavaScript, Custom Elements, Shadow DOM, HTML, Frontend.