This template incorporates best practices from Google’s Custom Element Best Practices guide. This document explains the patterns used in the template and why they’re important.
The shadow root is created in the constructor():
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
Why? The constructor is when you have exclusive knowledge of your element. Setting up implementation details here means you don’t need to guard against situations where your element is detached and reattached.
hidden attributeThe template includes proper support for the hidden attribute:
:host {
display: block;
}
:host([hidden]) {
display: none;
}
Why? Custom elements with a default display style will override the lower specificity built-in hidden attribute. Always add :host([hidden]) { display: none } to ensure hidden works as expected.
<slot> for content projectionThe template uses <slot> to allow users to pass content:
<slot></slot>
Why? This makes your component more composable. When custom elements aren’t supported, nested content remains visible and accessible (progressive enhancement).
Properties and attributes are kept in sync:
get exampleAttribute() {
return this.getAttribute('example-attribute');
}
set exampleAttribute(value) {
if (value === null || value === undefined) {
this.removeAttribute('example-attribute');
} else {
this.setAttribute('example-attribute', value);
}
}
Why? Users can interact with your element either declaratively (HTML attributes) or imperatively (JavaScript properties). Keeping them in sync prevents confusion and bugs.
attributeChangedCallbackThe attributeChangedCallback only handles side effects, not the full state update:
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) return;
switch (name) {
case 'example-attribute':
// Only handle side effects here
// The property getter reads from the attribute
break;
}
}
Why? If attributeChangedCallback also sets the property, and the property setter reflects to the attribute, you create an infinite loop. Instead, let the property getter read from the attribute directly.
The _upgradeProperty() method handles properties set before the element was defined:
_upgradeProperty(prop) {
if (Object.prototype.hasOwnProperty.call(this, prop)) {
const value = this[prop];
delete this[prop];
this[prop] = value;
}
}
Called in connectedCallback():
connectedCallback() {
this._upgradeProperty('exampleAttribute');
// ...
}
Why? Frameworks may set properties on your element before its definition loads. This pattern preserves those early-set values.
Always check if global attributes are already set before applying defaults:
connectedCallback() {
if (!this.hasAttribute('role')) {
// Only set if not already defined
// this.setAttribute('role', 'group');
}
if (!this.hasAttribute('tabindex')) {
// this.setAttribute('tabindex', 0);
}
}
Why? Developers using your element may need to override role, tabindex, etc. for accessibility. Always respect their choices.
Dispatch events when your component’s state changes internally:
// Dispatch when component changes its own state
this.dispatchEvent(new CustomEvent('component-name:change', {
detail: { value: newValue },
bubbles: true,
composed: true
}));
Don’t dispatch events when the host sets a property:
// ❌ BAD - creates infinite loops
set myProperty(value) {
this.setAttribute('my-property', value);
this.dispatchEvent(new CustomEvent('change')); // DON'T DO THIS
}
Why? The host already knows it set the property. Dispatching an event can cause infinite loops with data binding systems.
// ✅ Good - rich data as property
set items(value) {
this._items = value;
this.render();
}
// ❌ Bad - trying to accept objects as attributes
set items(value) {
this.setAttribute('items', JSON.stringify(value)); // Don't do this
}
Why? Serializing objects to strings is expensive and loses references. HTML attributes work best for primitive values.
This is a template repository. Before use:
npm run setup to replace placeholdersCOMPONENT-NAME → your element name (e.g., my-button)ComponentNameElement → your class name (e.g., MyButtonElement)COMPONENT_DESCRIPTION → your descriptionThe test file demonstrates all the best practices:
Run tests with:
npm test # Watch mode
npm run test:run # Single run
MIT - See LICENSE