Custom Elements
Custom Elements let you define your own HTML tags with custom behavior, properties, methods, and events.
Defining a Custom Element
class MyElement extends HTMLElement {
constructor() {
super(); // always call super() first
// Initial setup (but DOM may not be ready yet)
}
connectedCallback() {
// DOM is ready โ render here
}
}
customElements.define('my-element', MyElement);
Demo: Custom Element with Template
HTML
<profile-card name='Alice' role='Developer'></profile-card>
<profile-card name='Bob' role='Designer'></profile-card>
<pre id='ce-out' style='background:#f1f5f9;padding:12px;border-radius:6px;'></pre> JavaScript
class ProfileCard extends HTMLElement {
constructor() {
super();
this.innerHTML = '<div class='card'></div>';
}
connectedCallback() {
const name = this.getAttribute('name') || 'Unknown';
const role = this.getAttribute('role') || 'User';
const card = this.querySelector('.card');
card.style.cssText = 'padding:12px;background:#eef2ff;border-radius:8px;border:1px solid #c7d2fe;margin:4px 0;';
card.innerHTML = '<strong>' + name + '</strong> โ <em>' + role + '</em>';
}
}
customElements.define('profile-card', ProfileCard);
document.getElementById('ce-out').textContent =
'Two <profile-card> elements created.\\n' +
'Each reads its 'name' and 'role' attributes.'; Live Output Window
Observed Attributes
To respond to attribute changes, declare which attributes to observe:
class Counter extends HTMLElement {
static get observedAttributes() {
return ['value'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'value') {
this.render(newValue);
}
}
render(value) {
this.textContent = `Count: ${value}`;
}
}
Demo: Observed Attributes
HTML
<custom-counter value='5'></custom-counter>
<div style='margin-top:8px;display:flex;gap:8px;'>
<button id='inc-value'>Increment</button>
<button id='dec-value'>Decrement</button>
</div>
<pre id='attr-ce-out' style='background:#f1f5f9;padding:12px;border-radius:6px;'></pre> JavaScript
class CustomCounter extends HTMLElement {
static get observedAttributes() { return ['value']; }
constructor() {
super();
this.style.cssText = 'display:block;padding:16px;background:#fef9c3;border-radius:8px;font-size:1.2rem;text-align:center;border:1px solid #eab308;';
}
attributeChangedCallback(name, old, val) {
if (name === 'value') this.render(val);
}
render(val) {
this.textContent = 'Counter: ' + val;
}
}
customElements.define('custom-counter', CustomCounter);
let count = 5;
document.getElementById('inc-value').onclick = function() {
const el = document.querySelector('custom-counter');
el.setAttribute('value', ++count);
document.getElementById('attr-ce-out').textContent = 'Value changed to: ' + count;
};
document.getElementById('dec-value').onclick = function() {
const el = document.querySelector('custom-counter');
el.setAttribute('value', --count);
document.getElementById('attr-ce-out').textContent = 'Value changed to: ' + count;
}; Live Output Window
Element Properties and Methods
Custom elements can expose properties and methods like any class:
class RatingWidget extends HTMLElement {
set value(val) {
this.setAttribute('value', val);
this.updateStars();
}
get value() {
return parseInt(this.getAttribute('value')) || 0;
}
reset() {
this.value = 0;
}
updateStars() {
// render stars based on this.value
}
}
Demo: Custom Element with Methods
HTML
<rating-widget value='3'></rating-widget>
<div style='margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;'>
<button id='set-rating-1'>Set to 1 Star</button>
<button id='set-rating-5'>Set to 5 Stars</button>
<button id='reset-rating'>Reset</button>
<button id='get-rating'>Get Value</button>
</div>
<pre id='rating-out' style='background:#f1f5f9;padding:12px;border-radius:6px;'></pre> JavaScript
class RatingWidget extends HTMLElement {
static get observedAttributes() { return ['value']; }
constructor() {
super();
this.style.cssText = 'display:block;padding:12px;background:white;border-radius:8px;border:1px solid #e2e8f0;text-align:center;';
}
get value() { return parseInt(this.getAttribute('value')) || 0; }
set value(val) { this.setAttribute('value', val); }
attributeChangedCallback(name, old, val) {
if (name === 'value') this.render();
}
render() {
const val = this.value;
this.innerHTML = '';
for (let i = 1; i <= 5; i++) {
const star = document.createElement('span');
star.textContent = i <= val ? 'โ
' : 'โ';
star.style.cssText = 'font-size:1.5rem;cursor:pointer;color:' + (i <= val ? '#eab308' : '#cbd5e1') + ';margin:0 2px;';
star.dataset.rating = i;
star.onclick = () => { this.value = i; };
this.appendChild(star);
}
}
}
customElements.define('rating-widget', RatingWidget);
const widget = document.querySelector('rating-widget');
const out = document.getElementById('rating-out');
document.getElementById('set-rating-1').onclick = () => { widget.value = 1; out.textContent = 'Rating set to 1'; };
document.getElementById('set-rating-5').onclick = () => { widget.value = 5; out.textContent = 'Rating set to 5'; };
document.getElementById('reset-rating').onclick = () => { widget.value = 0; out.textContent = 'Rating reset'; };
document.getElementById('get-rating').onclick = () => { out.textContent = 'Current rating: ' + widget.value; }; Live Output Window
Dispatching Events
Custom elements can fire custom events:
class ToggleSwitch extends HTMLElement {
connectedCallback() {
this.addEventListener('click', () => {
const active = this.hasAttribute('active');
this.toggleAttribute('active');
this.dispatchEvent(new CustomEvent('toggle', {
detail: { active: !active },
bubbles: true
}));
});
}
}
Styling Custom Elements
Apply default styles in the constructor or connectedCallback:
class StyledButton extends HTMLElement {
connectedCallback() {
this.style.cssText = `
display: inline-block;
padding: 8px 16px;
background: #6366f1;
color: white;
border-radius: 6px;
cursor: pointer;
`;
}
}
Element Name Requirements
- Must contain a hyphen (
my-button,app-header,x-count) - One hyphen minimum, at least two characters before the hyphen
- Cannot be a single word like
buttonordialog - Case-sensitive (by convention, lowercase with hyphens)
Key Takeaways
- Extend
HTMLElementand callsuper()in the constructor - Define observed attributes with
static get observedAttributes() - Use
connectedCallbackfor DOM setup (not the constructor) - Expose properties (getter/setter) and methods on the class
- Dispatch custom events for user interactions
- Element names must include a hyphen
- Use
this.innerHTMLor DOM methods to render content (or Shadow DOM for encapsulation)