Custom Elements are really useful! They make reusing markup for functionality across your site as easy as adding an element to a page. Most of the controls I list on this site (and I think all of them at time of writing) are built with custom elements. Over time I've developed a template for new custom elements that contains all of the boilerplate structure and functionality I find generally useful, which is what I'm sharing here. 🙂

Once you've filled in its name, to use the custom element you'll just need to add the code to a JavaScript file loaded onto your page. I recommend putting it in its own file so it can be loaded specifically as-needed.

After you've copied the code, you'll want to find and replace all instances of TEMPLATE with a name (I recommend Title Case for this, like SwitchControl or TabsElement). You'll also want to set the static Name variable to the actual name of your element - (something like gw-switch or gw-check-listbox).

/**
 * @file 
 * @author 
 */
 
(function CustomElements(ns) {
	ns.TEMPLATE = class TEMPLATE extends HTMLElement {
		static InstanceCount = 0; // Global count of instances created
		static InstanceMap = {}; // Dynamic map of IDs to instances of the element currently attached

		// Element name (see MDN)
		static Name = "gw-template";

		// Attributes whose changes we respond to
		static observedAttributes = [];

		// Element CSSStyleSheet
		static #CommonStyleSheet = new CSSStyleSheet();
		static #CommonStyleAttribute = `data-${TEMPLATE.Name}-style`;
		static {
			TEMPLATE.#CommonStyleSheet.replaceSync(`${TEMPLATE.Name} {
			}`);
		}

		InstanceId; // Identifier for this instance of the element
		IsInitialized; // Whether the element has rendered its content

		#StyleSheet; // CSSStyleSheet for this instance
		#StyleAttribute; // Identifying attribute for this instance's CSSStyleSheet

		/** Creates an instance */
		constructor() {
			super();
			if(!this.getId) {
				// We're not initialized correctly. Attempting to fix:
				Object.setPrototypeOf(this, customElements.get(TEMPLATE.Name).prototype);
			}
			this.InstanceId = TEMPLATE.InstanceCount++;

			this.#StyleSheet = new CSSStyleSheet();
			this.#StyleAttribute = `data-${this.getId("style")}`;
		}

		/** Shortcut for the root node of the element */
		get Root() {
			return this.getRootNode();
		}
		/** Looks up the <head> element (or a fascimile thereof in the shadow DOM) for the element's root */
		get Head() {
			if(this.Root.head) {
				return this.Root.head;
			}
			if(this.Root.getElementById("gw-head")) {
				return this.Root.getElementById("gw-head");
			}
			const head = document.createElement("div");
			head.setAttribute("id", "gw-head");
			this.Root.prepend(head);
			return head;
		}

		/**
		 * Generates a globally unique ID for a key unique to the custom element instance
		 * @param {String} key Unique key within the custom element
		 * @returns A globally unique ID
		 */
		getId(key) {
			return `${TEMPLATE.Name}-${this.InstanceId}-${key}`;
		}
		/**
		 * Finds an element within the custom element created with an ID from getId
		 * @param {String} key Unique key within the custom element
		 * @returns The element associated with the key
		 */
		getRef(key) {
			return this.querySelector(`#${CSS.escape(this.getId(key))}`);
		}

		/** Handler invoked when the element is attached to the page */
		connectedCallback() {
			this.onAttached();
		}
		/** Handler invoked when the element is moved to a new document via adoptNode() */
		adoptedCallback() {
			this.onAttached();
		}
		/** Handler invoked when the element is disconnected from the document */
		disconnectedCallback() {
			delete TEMPLATE.InstanceMap[this.InstanceId];
		}
		/** Handler invoked when any of the observed attributes are changed */
		attributeChangedCallback(name, oldValue, newValue) {
			
		}

		/** Performs setup when the element has been sited */
		onAttached() {
			if(!this.Head.hasAttribute(TEMPLATE.#CommonStyleAttribute)) {
				this.Head.setAttribute(TEMPLATE.#CommonStyleAttribute, "");
				this.Root.adoptedStyleSheets.push(TEMPLATE.#CommonStyleSheet);
			}
			if(!this.Head.hasAttribute(this.#StyleAttribute)) {
				this.Head.setAttribute(this.#StyleAttribute, "");
				this.Root.adoptedStyleSheets?.push(this.#StyleSheet);
			}
			this.setAttribute("data-instance", this.InstanceId);

			TEMPLATE.InstanceMap[this.InstanceId] = this;
			if(document.readyState === "loading") {
				document.addEventListener("DOMContentLoaded", () => {
					this.#initialize();
				});
			}
			else {
				this.#initialize();
			}
		}

		/** First-time setup */
		#initialize() {
			if(this.IsInitialized) { return; }

			this.innerHTML = ``;

			this.IsInitialized = true;
		}
	}
	if(!customElements.get(ns.TEMPLATE.Name)) {
		customElements.define(ns.TEMPLATE.Name, ns.TEMPLATE);
	}
}) (window.CustomElements = window.CustomElements || {});