Last Updated: November 21, 2025
Custom Elements
class MyElement extends HTMLElement
Define custom element class
customElements.define('my-element', MyElement)
Register custom element
customElements.get('my-element')
Get element constructor
customElements.whenDefined('my-element')
Promise resolves when element defined
customElements.upgrade(element)
Manually upgrade element
class MyButton extends HTMLButtonElement
Extend built-in element (customized built-in)
define('my-button', MyButton, { extends: 'button' })
Register customized built-in element
<button is="my-button">
Use customized built-in element
Lifecycle Callbacks
constructor()
Element instance created
connectedCallback()
Element inserted into DOM
disconnectedCallback()
Element removed from DOM
adoptedCallback()
Element moved to new document
attributeChangedCallback(name, old, new)
Observed attribute changed
static get observedAttributes()
Return array of attributes to observe
static observedAttributes = ['name', 'value']
Define observed attributes (ES2022+)
Shadow DOM
this.attachShadow({ mode: 'open' })
Create open shadow root
this.attachShadow({ mode: 'closed' })
Create closed shadow root (not accessible)
this.shadowRoot
Access shadow root (if mode: 'open')
shadowRoot.innerHTML = '<div>...</div>'
Set shadow DOM content
element.shadowRoot
Access element's shadow root externally
attachShadow({ delegatesFocus: true })
Delegate focus to first focusable element
attachShadow({ slotAssignment: 'manual' })
Manual slot assignment mode
slot.assign([node1, node2])
Manually assign nodes to slot
Templates
<template id="my-template">
Define reusable template
template.content
Access template's DocumentFragment
template.content.cloneNode(true)
Clone template content
document.querySelector('#my-template')
Get template element
shadowRoot.appendChild(clone)
Insert template into shadow DOM
Slots
<slot></slot>
Default slot for light DOM content
<slot name="header"></slot>
Named slot
<div slot="header">Content</div>
Assign content to named slot
<slot>Fallback</slot>
Fallback content for empty slot
slot.assignedNodes()
Get nodes assigned to slot
slot.assignedElements()
Get elements assigned to slot (no text nodes)
assignedNodes({ flatten: true })
Include nested slot assignments
element.assignedSlot
Get slot an element is assigned to
@slotchange event
Fires when slot's assigned nodes change
Attributes & Properties
this.getAttribute('name')
Get attribute value
this.setAttribute('name', 'value')
Set attribute value
this.removeAttribute('name')
Remove attribute
this.hasAttribute('name')
Check if attribute exists
this.toggleAttribute('name', force)
Toggle boolean attribute
get name() { return this._name }
Define property getter
set name(val) { this._name = val }
Define property setter
this.setAttribute('name', this.name)
Reflect property to attribute
Styling
:host { color: blue }
Style shadow host element
:host(.active) { }
Style host with specific class
:host([disabled]) { }
Style host with attribute
:host-context(.dark) { }
Style based on ancestor selector
::slotted(*) { }
Style slotted elements
::slotted(p) { }
Style specific slotted elements
::part(name) { }
Style element parts from outside
<div part="name">
Expose element part for styling
var(--custom-property)
CSS custom properties pierce shadow DOM
@import url('styles.css')
Import external styles in shadow DOM
Events
this.dispatchEvent(new Event('change'))
Dispatch standard event
new CustomEvent('my-event', { detail: data })
Dispatch event with custom data
{ bubbles: true }
Make event bubble up DOM tree
{ composed: true }
Make event cross shadow boundaries
{ cancelable: true }
Allow event to be cancelled
event.composedPath()
Get full event path including shadow DOM
this.addEventListener('click', handler)
Add event listener
this.removeEventListener('click', handler)
Remove event listener
Form Integration
static formAssociated = true
Enable form association
this.attachInternals()
Get ElementInternals for form features
internals.setFormValue(value)
Set form value
internals.setValidity(flags, message)
Set validation state
internals.form
Access associated form element
formAssociatedCallback(form)
Called when associated with form
formDisabledCallback(disabled)
Called when disabled state changes
formResetCallback()
Called when form is reset
formStateRestoreCallback(state, mode)
Called when form state restored
Best Practices
if (!customElements.get('my-element'))
Check if element already defined
super() in constructor
Always call super() first in constructor
:defined pseudo-class
Style defined custom elements
:not(:defined) { visibility: hidden }
Hide undefined elements (prevent FOUC)
Element.prototype.isConnected
Check if element is in DOM
💡 Pro Tip:
Use mode: 'open' for shadow DOM to enable browser DevTools inspection. Always set composed: true for custom events that need to cross shadow boundaries!