Polymer Custom Elements — Creating Reusable Custom HTML Elements
In this tutorial, you will learn about Polymer Custom Elements. We cover key concepts, practical examples, and best practices to help you master this topic.
Custom Elements are the core of Web Components — they let you define new HTML elements with custom behavior, lifecycle hooks, and reactive attribute handling using the standard CustomElementRegistry API.
What You'll Learn
- Defining custom elements with customElements.define
- Lifecycle callbacks (connected, disconnected, attributeChanged)
- Observed attributes and reactions
- Element properties and methods
- Extending native HTML elements
Why It Matters
Custom Elements let you create your own HTML vocabulary. Instead of <div class="user-card">, you write <user-card>. The browser handles element registration, lifecycle, and attribute observation natively.
Real-World Use
A UI library with <custom-button>, <custom-dialog>, <custom-tabs>, and <custom-table> elements that encapsulate behavior and styling, used across multiple applications.
Custom Element Architecture
flowchart TD
A[Custom Element] --> B[Definition]
A --> C[Lifecycle]
A --> D[Attributes]
A --> E[Properties]
B --> F[Class]
B --> G[Registration]
C --> H[constructor]
C --> I[connectedCallback]
C --> J[disconnectedCallback]
C --> K[attributeChangedCallback]
D --> L[observedAttributes]
D --> M[Reaction]
E --> N[Getters/Setters]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Basic Custom Element
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.avatar {
width: 50px; height: 50px; border-radius: 50%;
background: #1a237e; color: white;
display: flex; align-items: center; justify-content: center;
font-size: 20px; font-weight: bold;
}
.name { font-size: 16px; font-weight: bold; }
.email { font-size: 14px; color: #666; }
</style>
<div class="card">
<div class="avatar">${this.getAttribute('name')?.charAt(0) || '?'}</div>
<div>
<div class="name">${this.getAttribute('name') || 'Unknown'}</div>
<div class="email">${this.getAttribute('email') || ''}</div>
</div>
</div>
`;
}
}
customElements.define('user-card', UserCard);
<user-card name="Alice Johnson" email="alice@example.com"></user-card>
Expected output: A user card with avatar initial, name, and email. The element registers as <user-card> and initializes on first connection.
Lifecycle Callbacks
class LifecycleElement extends HTMLElement {
constructor() {
super();
this.timer = null;
}
static get observedAttributes() {
return ['status', 'value'];
}
connectedCallback() {
this._render();
this.timer = setInterval(() => this._tick(), 1000);
}
disconnectedCallback() {
clearInterval(this.timer);
}
attributeChangedCallback(name, oldValue, newValue) {
if (this.isConnected) this._render();
}
_tick() {
const val = parseInt(this.getAttribute('value') || '0');
this.setAttribute('value', String(val + 1));
}
_render() {
if (!this.shadowRoot) this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<p>Status: ${this.getAttribute('status') || 'unknown'}</p>
<p>Value: ${this.getAttribute('value') || '0'}</p>
`;
}
}
customElements.define('lifecycle-el', LifecycleElement);
Expected output: The element logs lifecycle events. Attributes trigger attributeChangedCallback. A timer increments value every second.
Observed Attributes
class ProgressBar extends HTMLElement {
static get observedAttributes() {
return ['value', 'max', 'color'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() { this._render(); }
attributeChangedCallback() { this._render(); }
_render() {
const value = parseFloat(this.getAttribute('value') || '0');
const max = parseFloat(this.getAttribute('max') || '100');
const percent = Math.min(100, Math.max(0, (value / max) * 100));
const color = this.getAttribute('color') || '#4a90d9';
this.shadowRoot.innerHTML = `
<div class="progress">
<div class="bar" style="width:${percent}%"></div>
</div>
<div class="label">${Math.round(percent)}%</div>
`;
}
get value() { return parseFloat(this.getAttribute('value') || '0'); }
set value(val) { this.setAttribute('value', String(val)); }
get max() { return parseFloat(this.getAttribute('max') || '100'); }
set max(val) { this.setAttribute('max', String(val)); }
}
customElements.define('progress-bar', ProgressBar);
Expected output: A progress bar updates when value, max, or color attributes change. Property getters/setters provide attribute Reflection.
Element Properties and Methods
class CounterElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._count = 0;
}
get count() { return this._count; }
set count(val) {
this._count = val;
this._update();
this.dispatchEvent(new CustomEvent('count-changed', {
detail: { count: val }
}));
}
increment() { this.count++; }
decrement() { this.count--; }
reset() { this.count = 0; }
connectedCallback() {
this._render();
this._setupListeners();
}
disconnectedCallback() {
this.shadowRoot.querySelector('#inc').removeEventListener('click', this._inc);
this.shadowRoot.querySelector('#dec').removeEventListener('click', this._dec);
}
_setupListeners() {
this._inc = () => this.increment();
this._dec = () => this.decrement();
this.shadowRoot.querySelector('#inc').addEventListener('click', this._inc);
this.shadowRoot.querySelector('#dec').addEventListener('click', this._dec);
}
_render() {
this.shadowRoot.innerHTML = `
<button id="dec">-</button>
<span class="count">${this._count}</span>
<button id="inc">+</button>
<button id="reset">Reset</button>
`;
this.shadowRoot.querySelector('#reset').onclick = () => this.reset();
}
_update() {
const el = this.shadowRoot.querySelector('.count');
if (el) el.textContent = this._count;
}
}
customElements.define('counter-el', CounterElement);
Expected output: A counter with increment, decrement, and reset buttons. The count-changed event fires on every change.
Extending Native Elements
class ConfirmButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', this._onClick);
}
_onClick(event) {
if (!this.hasAttribute('no-confirm')) {
const message = this.getAttribute('confirm-message') || 'Are you sure?';
if (!confirm(message)) {
event.preventDefault();
event.stopPropagation();
}
}
}
setLoading(loading) {
this.disabled = loading;
this.textContent = loading ? 'Loading...' : (this.getAttribute('original-text') || 'Confirm');
}
connectedCallback() {
if (!this.getAttribute('original-text')) {
this.setAttribute('original-text', this.textContent);
}
}
}
customElements.define('confirm-button', ConfirmButton, { extends: 'button' });
<button is="confirm-button" confirm-message="Delete this item?">Delete</button>
Expected output: The confirm-button extends the native button. Clicking shows a confirmation dialog.
Common Mistakes
Forgetting hyphen in element name - Custom element names must contain a hyphen. Single-word names throw an error.
Calling attachShadow multiple times - attachShadow can only be called once. Check for existing shadow root.
Not using static observedAttributes - Without it, attributeChangedCallback never fires.
Setting attributes in constructor - Use connectedCallback for DOM operations.
Not cleaning up event listeners - Remove listeners in disconnectedCallback to prevent memory leaks.
Practice Questions
- How do you observe attribute changes on a custom element?
- What is the difference between connectedCallback and constructor?
- How do you define an element that extends a native HTML element?
- How do you reflect a JS property to an HTML attribute?
- How do you fire a custom event from within a custom element?
Challenge: Build a custom <star-rating> element with observed attributes for value (0-5) and size, click to set rating, hover preview, keyboard Accessibility, custom event on rating change, and proper cleanup.
FAQ
Mini Project
Build a form component library with: <form-input> (text input with label, validation, error message), <form-select> (dropdown with options), <form-checkbox> (styled checkbox), and <form-group> (validation summary).
What's Next
Custom Elements define the element. Learn how Polymer Shadow DOM provides DOM and style Encapsulation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro