Defining an Element
Extending HTMLElement
Section titled “Extending HTMLElement”Every autonomous custom element starts as an ES class that extends HTMLElement:
class HelloBadge extends HTMLElement { // lifecycle callbacks go here}The class can be anonymous or named — the name is only used internally by JavaScript. The public identity of the element is the tag name you register.
Registering with customElements.define
Section titled “Registering with customElements.define”The customElements.define method takes two required arguments and one optional argument:
customElements.define(tagName, constructor, options);// ^string ^class ^optional { extends: '...' }customElements.define('hello-badge', HelloBadge);After this call, the browser knows that every <hello-badge> in the document is an instance of HelloBadge.
The mandatory hyphen rule
Section titled “The mandatory hyphen rule”Custom element tag names must contain at least one hyphen. This is how the browser distinguishes your elements from current and future built-in elements:
<!-- valid custom element names --><hello-badge></hello-badge><my-app></my-app><x-button></x-button>
<!-- INVALID — no hyphen — the browser treats these as unknown built-ins --><hellobadge></hellobadge><mybadge></mybadge>Reserved names you cannot use, even though they contain a hyphen (they belong to SVG/MathML): annotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name, missing-glyph.
Using the element in HTML
Section titled “Using the element in HTML”Once defined, the element works like any native element — place it in markup, set attributes, query it with JavaScript:
<hello-badge name="Alice"></hello-badge><hello-badge name="Bob"></hello-badge>const badge = document.querySelector('hello-badge');badge.getAttribute('name'); // 'Alice'Runnable demo
Section titled “Runnable demo”Try changing the name attribute value in the HTML panel and watch the output update.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Native custom elements (customElements.define) | Not tied to any framework, works anywhere there’s a DOM, no build step required | DX isn’t as smooth as a framework like React/Vue — no JSX, no built-in reactive state, you wire it up yourself |
| Naming the tag by the hyphen rule | Lets the browser clearly distinguish your element from any current or future native element | You always have to think of a hyphenated name — a small tax on your naming convention |
Common mistakes
Section titled “Common mistakes”- Naming a tag without a hyphen — e.g.
mybadge. The browser rejects the registration or silently treats it as anHTMLUnknownElement. - Calling
customElements.define()twice with the same tag name — this throws immediately. CheckcustomElements.get(tagName)first if you’re not sure it’s already registered. - Using a reserved name — e.g.
font-face-srcorannotation-xmlcannot be registered because the spec reserves them for SVG/MathML.
💡 Real-world examples
GitHub — registers elements like
<relative-time>with a hyphenated name per spec, then uses it directly in real page HTML. (github/relative-time-element)Google Material Web — ships each Material Design component as its own module that calls
customElements.defineindependently, so consumers import and register only the components they use. (material-components/material-web)