Zach’s ugly mug (his face) Zach Leat­herman

?nodefine — a pattern to skip Custom Element definitions

519 00 February 14, 2025

The following code is a minimum viable custom element:

class Nimble extends HTMLElement {}

customElements.define("nim-ble", Nimble);

The define call is typically packaged up in the component code (for ease of use) and not in your application code. I typically adapt this into a static function like so:

class Nimble extends HTMLElement {
  static define(tagName) {
    customElements.define(tagName || "nim-ble", this);
  }
}

Nimble.define();

On first glance, this might not offer much benefit, but depending on the platform features I’m using I may also include a Cut-the-Mustard style feature test in there:

class Nimble extends HTMLElement {
  static define(tagName) {
		// Baseline 2020: Chrome 71 Safari 12.1 Firefox 65
		// (extended browser support on top of ESM and Custom Elements)
		if(typeof globalThis !== "undefined") {
			customElements.define(tagName || "nim-ble", this);
		}
  }
}

Nimble.define();

Automatic Definition

The biggest drawback I’m struggling with in these patterns is how to allow folks to opt-out of the customElements.define call when they import or bundle the script. Perhaps they might want to use a different tag name (you can only define a class once in the registry), or run some additional advanced configuration before definition.

Here’s how you would typically import the script code (or import "./nimble.js" works too):

<!-- <nim-ble> is defined for you -->
<script type="module" src="nimble.js"></script>

Here’s a new idea I’m experimenting with to allow opt-out of define() by adding a ?nodefine query parameter to the script URL:

<!-- <nim-ble> is *not* defined for you -->
<script type="module" src="nimble.js?nodefine"></script>
<script type="module">
Nimble.define("some-other-name");
</script>

And then in your component code you’ll check for ?nodefine before auto defining the custom element:

class Nimble extends HTMLElement {
  static define(tagName) {
    customElements.define(tagName || "nim-ble", this);
  }
}

// This line is the magic:
if(!(new URL(import.meta.url)).searchParams.has("nodefine")) {
  Nimble.define();
}

window.Nimble = Nimble;
export { Nimble };

This pattern works with import too:

<script type="module">
// <nim-ble> is *not* defined for you
import { Nimble } from "./nimble.js?nodefine";

Nimble.define("some-other-name");

// Or directly:
customElements.define("nim-ble", Nimble);
</script>

This would also work on any Custom Element code adopting this pattern nestled inside a larger bundle:

<script type="module" src="bundle-398720.js?nodefine"></script>

What do y’all think?

Community Addendums

Updated February 14, 2025 I somehow missed these excellent links which cover similar ground:


< Older
Blog Questions Challenge 2025
Newer >
Extract Colors from an Image for CSS Themes

Zach Leatherman IndieWeb Avatar for https://zachleat.com/is a builder for the web at Font Awesome and the creator of Build Awesome (née IndieWeb Avatar for https://www.11ty.devEleventy/11ty), an award-winning open source website generator. He measures website performance with speedlify and at one point became too fixated on web fonts. He has given 89 talks in nine different countries at events like Beyond Tellerrand, Smashing Conference, Jamstack Conf, CSSConf, and The White House. Formerly part of CloudCannon, Netlify, Filament Group, NEJS CONF, and NebraskaJS. Learn more about Zach »

Shamelessly plug your related post

These are webmentions via the IndieWeb and webmention.io.

Sharing on social media?

This is what will show up when you share this post on Social Media:

How did you do this? I automated my Open Graph images. (Peer behind the curtain at the test page)