Skip to content

Defining an Element

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.

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.

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.

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'

Try changing the name attribute value in the HTML panel and watch the output update.

OptionBenefitCost
Native custom elements (customElements.define)Not tied to any framework, works anywhere there’s a DOM, no build step requiredDX 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 ruleLets the browser clearly distinguish your element from any current or future native elementYou always have to think of a hyphenated name — a small tax on your naming convention
  • Naming a tag without a hyphen — e.g. mybadge. The browser rejects the registration or silently treats it as an HTMLUnknownElement.
  • Calling customElements.define() twice with the same tag name — this throws immediately. Check customElements.get(tagName) first if you’re not sure it’s already registered.
  • Using a reserved name — e.g. font-face-src or annotation-xml cannot 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.define independently, so consumers import and register only the components they use. (material-components/material-web)

Which tag name is a valid autonomous custom element name?
What is the first argument to customElements.define?
What does the browser do when it encounters a tag name that has been registered with customElements.define?