Zach’s ugly mug (his face) Zach Leatherman

?nodefine — a pattern to skip Custom Element definitions

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

Zach Leatherman IndieWeb Avatar for https://zachleat.com/is a builder for the web at Font Awesome and the creator/maintainer of IndieWeb Avatar for https://www.11ty.devEleventy (11ty), an award-winning open source site generator. At one point he became entirely too fixated on web fonts. He has given 84 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 »

11 Reposts

Rob MeyerStefan MateiCory LaViskaTyler StickaMichael WarrenLiang WenfengRyan MulliganTony WardLia Rei LoveJames loveJake Lazaroff

67 Likes

Fynn Ellie BeckerFrank // MottokroshKristóf PoduszlóFred BuxJordan RunningPelle WessmanBurton SmithDanEdaws404Stefan MateiKiaJhett TolentinoSteven BeshenskyAnthony Frehner❄️ Jim Schofield ☃️JauntyWunderKind???? Attila GondaCory LaViskaJosh CrainDoug GibsonE. Erdemsage ????Alex GuyotThomas CannonJWBlake Watson :prami:AndrewTyler StickaAshur CabreraDave ???? :cursor_pointer:John Kemp-CruzNikitaSage ????Jake LazaroffNathan KnowlerTony WardChris JohnsonMahirpatakOlivier ForgetJames loveRyan MulliganLia Rei LoveDorothy ???????? ???? DMsJoemr_frisby_drawsLindaMatt CloughLiang WenfengSedmirnelMichael WarrenHancockIKEA JesusJouni MännistöFrederick H. DanielsBrielleGucci BurrAndré RuffertRay GesualdoFranklin Thien
47 Comments
  1. Jake Lazaroff

    i arrived at a very similar place (and @knowler.dev before me, linked in the post)

  2. Zach Leatherman

    Amazing! How strongly do you feel about opt-in versus opt-out?

  3. Noah Liebman

    @zachleat @knowler did the same in reverse! https://knowler.dev/blog/to-define-custom-elements-or-not-when-distributing-them To define custom elements or not when distributing them – Nathan Knowler

  4. Zach Leatherman

    oh, dang you allow the tag name to be passed in the query param — interesting

  5. sage ????

    @zachleat seems sensible to me. i had the thought in the past of using ?tag=another-name, but exposing everything directly is more flexible

  6. Tony Ward

    Oh I dig this!

  7. Jake Lazaroff

    i like opt-in! and i think i actually prefer ?nodefine to ?define=false

  8. Nathan Knowler

    I tend to follow the principle of no side effects of stuff like this.

  9. Nathan Knowler

    s/of stuff/for stuff

  10. Zach Leatherman

    thanks Tony!

  11. Nathan Knowler

    “Avoiding bundlers at all costs” crew unite!

  12. Zach Leatherman

    That’s a good argument!

  13. Jake Lazaroff

    wait i meant opt-out lol

  14. Zach Leatherman

    @jakelazaroff.com has some lovely ideas documented here that overlap: til.jakelazaroff.com/html/define-...

  15. Jake Lazaroff

    yeah i think i feel more strongly about the principle of least surprise than about whether to define by default or not like, i personally prefer define by default, but if there were a community expectation of opt-in definition via a query string param i’d choose that pattern ove… Truncated

  16. Joel Drake

    I read it as Node Fine. ???? How about camel casing it? ?noDefine

  17. Zach Leatherman

    I personally don’t like relying on case sensitivity in query param keys (acknowledging that they are case sensitive) ????

  18. Doug Parker ????️

    @zachleat FYI the static `define` pattern you describe is a proposed community protocol. https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/on-demand-definitions.md My hope is that tools and frameworks can use this knowledge to auto-generate the `.defin… Truncated

  19. Doug Parker ????️

    @zachleat I'm curious, how does this pattern work with bundlers? I guess you can still `import { Nimble } from './nimble.js?nodefine';` and presumably `import.meta` will be set correctly. Will bundlers tree-shake the `customElements.define` call? Will they tree-shake… Truncated

  20. Nikita

    @zachleat I discovered this approach for me, when I was trying to replicate petite-vue's `init`, but in ESM https://github.com/vuejs/petite-vue?tab=readme-ov-file#usage GitHub - vuejs/petite-vue: 6kb subset of Vue optimized for progressive enhancement

  21. Zach Leatherman :11ty:

    @kytta I think that is loading a completely different file (with `export` included) but yeah the query param thing is nice!

  22. Zach Leatherman :11ty:

    @develwithoutacause some good discussion on the bundler question documented here from @jakelazaroff which I somehow missed: https://til.jakelazaroff.com/html/define-a-custom-element/ [html] Define a custom element | Today I Learned

  23. Zach Leatherman :11ty:

    @bano yeah! this post agrees with you: https://til.jakelazaroff.com/html/define-a-custom-element/ [html] Define a custom element | Today I Learned

  24. Zach Leatherman :11ty:

    @wavebeem yeah I’m seeing that in a few places too!

  25. Zach Leatherman :11ty:

    @develwithoutacause ooh this is lovely — thank you! I’ll link it up from my post

  26. McNeely

    @develwithoutacause @zachleat wow this is an awesome proposal! Here's hoping it really gets traction ????

  27. Doug Parker ????️

    @zachleat @jakelazaroff Ooh duplicating the module between `?define` and not seems pretty rough, though it seems like that's an issue even outside a bundler? Limiting all consumers to either `?define` or not seems pretty constraining at scale.

  28. sage ????

    @zachleat i've been a little unsure how this works out in practice though... like what's the use case for defining it with a new name? i guess if you just don't like the name that could be useful, and as long as it's not part of a larger system of components with … Truncated

  29. Rob Meyer

    I've been craving standardization one layer up (when should the script be fetched) -- I see several useful settings: (1) deferring script loading until the (not yet defined) custom element nears the viewport, (2) defer until used in DOM, (3) defer until idle, (4)...

  30. Rob Meyer

    I *feel* like it should be opt-in to avoid unwanted surprises, but assuming the query string arguments work well with all major bundlers, it's easy enough to opt-out that defaulting to define isn't a major hang-up for me.

  31. Zach Leatherman :11ty:

    @develwithoutacause @jakelazaroff well if you’re wanting both mechanisms you probably wouldn’t do import "./nimble.js" with import "./nimble.js?define", you’d do import "./nimble.js" with `Nimble.define()`—right? (which to me seems like yet another vote f… Truncated

  32. Zach Leatherman :11ty:

    @wavebeem for sure — it’s just a preference for small projects. I can see the argument that you might not want renames for larger projects

  33. Doug Parker ????️

    @zachleat @jakelazaroff Sure, you can not use the feature, but you need to *know* you can't use it in that situation. Any given file can import `?define` but it needs to be confident that no *other* file imports without `?define`, which seems hard to enforce. I guess you co… Truncated

  34. Zach Leatherman :11ty:

    @develwithoutacause @jakelazaroff you lost me at bundler *runs away*

  35. Konnor Rogers

    "And all of the bundlers cried out in agony"

  36. Zach Leatherman

    I don’t know her!!

  37. ❄️ Jim Schofield ☃️

    Super nifty. Is there a reason you assigned to the window?

  38. Nathan Knowler

    It mirrors the availability of constructors for built-in elements, which is useful for `instanceof` checks (e.g. `event.target instanceof HTMLFormElement`).

  39. Nathan Knowler

    I was going back and forth about whether I would drop this from my own static `define()` method recently, because you can get the constructor with `customElements.get()`, but ultimately I decided to keep it because for me `define()` is meant to set it up like a built-in.

  40. Doug Parker ????️

    @zachleat This came across my feed again and I randomly read it as `node fine?`

  41. Danny Engelman ????????️????️

    > opt-out of define > want different tag name > run script before definition Why create a behemoth of a Web Component that caters for IFs that will never happen? it is OOP ????????????????????????????????????????????????????????.????????????????????????("????????… Truncated

  42. Stefan Matei

    Love this! And it looks like it would make 3rd party components more immediately compatible with my little definer.js script if I changed it to add `.js?nodefine&define=false` (so it works with both your and @jakelazaroff.com’s patterns) instead of just adding the .js extensi… Truncated

  43. Danny Engelman ????????️????️

    I extended Josh Wardle his Wordle (written with Web Components) dev.to/dannyengelma...

  44. Burton Smith

    That way you can import the module as-is from the definer from one module or you can import the class from another module if you want to customize the tag or extend the class.

  45. Burton Smith

    I like the idea of self-contained definers, especially if you have custom logic for defining components. Because of the side-effect that comes with defining components in the same module as where the logic is, I've been a big proponent of splitting them into separate modules… Truncated

  46. Cory Rylan

    The way I've handled this is by making a secondary/separate import that auto defines the custom element. github.com/coryrylan/re...

  47. Pelle Wessman

    Such a lovely blast from the past! It hadn’t crossed my mind that the ”import.meta.url” could be a replacement for the old way of finding the data by checking the DOM for script tags (and when synchronously loading a script, grabbing the last available element in the DOM or what… Truncated

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)