?nodefine — a pattern to skip Custom Element definitions
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:
47 Comments
Jake Lazaroff
i arrived at a very similar place (and @knowler.dev before me, linked in the post)
Zach Leatherman
Amazing! How strongly do you feel about opt-in versus opt-out?
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
Zach Leatherman
oh, dang you allow the tag name to be passed in the query param — interesting
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
Tony Ward
Oh I dig this!
Jake Lazaroff
i like opt-in! and i think i actually prefer ?nodefine to ?define=false
Nathan Knowler
I tend to follow the principle of no side effects of stuff like this.
Nathan Knowler
s/of stuff/for stuff
Zach Leatherman
thanks Tony!
Nathan Knowler
“Avoiding bundlers at all costs” crew unite!
Zach Leatherman
That’s a good argument!
Jake Lazaroff
wait i meant opt-out lol
Zach Leatherman
@jakelazaroff.com has some lovely ideas documented here that overlap: til.jakelazaroff.com/html/define-...
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
Joel Drake
I read it as Node Fine. ???? How about camel casing it? ?noDefine
Zach Leatherman
I personally don’t like relying on case sensitivity in query param keys (acknowledging that they are case sensitive) ????
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
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
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
Zach Leatherman :11ty:
@kytta I think that is loading a completely different file (with `export` included) but yeah the query param thing is nice!
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
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
Zach Leatherman :11ty:
@wavebeem yeah I’m seeing that in a few places too!
Zach Leatherman :11ty:
@develwithoutacause ooh this is lovely — thank you! I’ll link it up from my post
McNeely
@develwithoutacause @zachleat wow this is an awesome proposal! Here's hoping it really gets traction ????
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.
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
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)...
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.
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
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
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
Zach Leatherman :11ty:
@develwithoutacause @jakelazaroff you lost me at bundler *runs away*
Konnor Rogers
"And all of the bundlers cried out in agony"
Zach Leatherman
I don’t know her!!
❄️ Jim Schofield ☃️
Super nifty. Is there a reason you assigned to the window?
Nathan Knowler
It mirrors the availability of constructors for built-in elements, which is useful for `instanceof` checks (e.g. `event.target instanceof HTMLFormElement`).
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.
Doug Parker ????️
@zachleat This came across my feed again and I randomly read it as `node fine?`
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
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
Danny Engelman ????????️????️
I extended Josh Wardle his Wordle (written with Web Components) dev.to/dannyengelma...
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.
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
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...
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