What rules does customElements.define() enforce on the tag name and the class you pass it, and what happens when you break them?
answer
- hyphen is a contract, not a style rule
- reserved namespace for future HTML
- two different DOMException types
- one class, one name, one time
- get() before define() in shared bundles
basics
~10 scustomElements.define(name, class) requires a lowercase name that contains a hyphen and a class that extends HTMLElement. An invalid name throws SyntaxError; registering a name twice, or one class under two names, throws NotSupportedError.
solid answer
~50 s`customElements.define('my-card', MyCard)` registers a tag name in the document's custom element registry. The name must start with an ASCII lowercase letter, contain no uppercase, and contain at least one hyphen — the hyphen is what guarantees the name can never collide with a future built-in HTML element, and it is also why the parser treats an unknown hyphenated tag as an upgradeable `HTMLElement` rather than `HTMLUnknownElement`. A short list of hyphenated names is reserved (`annotation-xml`, `color-profile`, `font-face`, `missing-glyph` and a few others). The second argument must be a class that ultimately extends `HTMLElement`. Registration is once-only and irreversible: an invalid name throws a `SyntaxError`, defining the same name twice or the same constructor under two names throws a `NotSupportedError`, and there is no way to un-define. `customElements.get('my-card')` tells you whether a name is already taken.
go deeper
Be ready to write the two-line define call from memory and state the name rules: lowercase, must contain a hyphen, extends HTMLElement. Say plainly that the hyphen prevents collisions with built-in HTML elements.
Explain the mechanics behind the rules: which exception each violation throws, why a hyphenated unknown tag is an HTMLElement awaiting upgrade rather than HTMLUnknownElement, and what customElements.get and whenDefined are for.
Show that you have hit the once-only registry in production — duplicate definitions from a twice-loaded bundle or a dev-server reload — and describe the get()-before-define guard plus what it silently trades away when two versions collide.
Own the naming policy: who may claim a tag name across teams, whether components ship prefixed or version-suffixed names, and how you keep a shared library from being registered twice on a host page you do not control.
## What define() is for `customElements.define()` is the single entry point that turns a JavaScript class into a real HTML element. After it runs, the browser knows that every `<my-card>` in the document — past, present and future — is an instance of your class, gets your prototype and your methods, and receives your lifecycle callbacks. Before it runs, the same tag is just a generic element the browser is willing to keep around. It takes two required arguments and one optional one: ```js class MyCard extends HTMLElement {} customElements.define('my-card', MyCard); ``` ## The name rules A *valid custom element name* must: - start with an ASCII lowercase letter (`a`–`z`), - contain at least one hyphen (`-`), - contain no ASCII uppercase letters, - not be one of the reserved names: `annotation-xml`, `color-profile`, `font-face`, `font-face-src`, `font-face-uri`, `font-face-format`, `font-face-name`, `missing-glyph`. Those already have meaning in SVG and MathML. So `my-card`, `x-1`, and `acme-user-badge` are legal; `mycard`, `MyCard`, `1-card` and `font-face` are not. A bad name throws a `SyntaxError` `DOMException` at the `define()` call. The hyphen is not a naming convention — it is a forward-compatibility contract. HTML reserves the entire hyphen-free namespace for itself, so a future `<carousel>` element added to the standard can never break your page, and your `<acme-carousel>` can never be redefined out from under you. ## What the parser does with an unknown hyphenated tag This rule also has a concrete parsing consequence. A garbage tag such as `<blah>` becomes an `HTMLUnknownElement`. A hyphenated tag such as `<my-card>` becomes an `HTMLElement` marked internally as an *undefined custom element*: it sits in the DOM, keeps its attributes and children, and is a candidate for upgrade the moment a matching `define()` call happens. CSS can see the difference — `my-card:not(:defined)` matches only while it is still waiting, which is the standard way to hide un-upgraded elements and avoid a flash of unstyled content. ## The class rules The constructor argument must be a class (something constructable, not an arrow function) whose prototype chain ends at `HTMLElement`. Extending `HTMLElement` directly gives you an *autonomous* custom element. Extending a specific interface such as `HTMLButtonElement` gives you a *customized built-in*, and then you must also pass `{ extends: 'button' }` as the third argument so the registry knows which tag it decorates. ## Registration is once-only The registry is append-only for the lifetime of the document: - Defining a name that is already registered throws `NotSupportedError`. - Registering the *same constructor* under two different names also throws `NotSupportedError` — one class, one tag. - There is no `customElements.undefine()` and no way to swap an implementation. That matters in practice more than it sounds. Any bundle that ships a component library and might be loaded twice on one page — an app plus a widget, two micro-frontends, a hot-reloading dev server — will hit the duplicate-definition throw. The common guard is to check first: ```js if (!customElements.get('my-card')) { customElements.define('my-card', MyCard); } ``` This silently keeps whichever copy won the race, which is a tradeoff, not a fix: the two copies may be different versions. ## Related registry methods - `customElements.get(name)` → the constructor, or `undefined` if the name is free. - `customElements.whenDefined(name)` → a promise that resolves with the constructor once the name is registered; useful for code that must wait for a definition it does not own. - `customElements.upgrade(root)` → forces upgrade of already-created elements in a subtree, instead of waiting for them to be inserted into the document. None of these let you replace a definition. Registration is a one-way door, and designing around that — versioned tag names, a single owner per name — is part of shipping custom elements at scale.
- Before any definition is registered, what does the DOM create for a `<my-card>` element written in the HTML source?An ordinary `HTMLElement` instance flagged as an undefined custom element — not `HTMLUnknownElement`, which is what a non-hyphenated unknown tag gets. It keeps its attributes and children, matches `:not(:defined)` in CSS, and is upgraded to your class as soon as a matching `define()` runs.
- Can you register one class under two tag names, for example an alias?No. `customElements.define()` throws `NotSupportedError` if the constructor is already registered under another name. If you genuinely need two tags, define a trivial subclass for the second name, or generate the class from a factory function so each name gets its own constructor.
- Why do component libraries wrap `define()` in an `if (!customElements.get(name))` guard?Because a duplicate definition throws and would break the whole script. If two copies of a library load on one page — an app plus an embedded widget, or a dev server reload — the guard keeps the first registration and skips the second. It is a crash guard, not a version-conflict solution: the surviving copy may be the wrong version.
The registry is like a domain-name registrar for tag names: hyphenated names are the public zone anyone may claim, hyphen-free names are reserved for the standard itself, and once you claim a name it is yours for the life of the page — no transfers, no re-registrations.
saying these in an interview costs you the question
- Thinks the hyphen is a naming convention you can skip
- Believes you can redefine or un-define a registered tag name
- Says an unknown hyphenated tag becomes HTMLUnknownElement
- Registers one class under several tag names as an alias
- Thinks define() must run before the element appears in the HTML