HTMLCustom Elements Overview

Custom Elements Overview

The Custom Elements API lets you define your own HTML tags — like <user-card> or <fancy-button> — backed by a JavaScript class. Once registered, a custom element behaves like any built-in element: the browser parses it, you can style it with CSS, and it participates in the normal DOM lifecycle. This is the foundation of the Web Components family, alongside Shadow DOM, <template>, and <slot>.

Registering an Element with customElements.define

You register a custom element by calling customElements.define(name, class), passing the tag name and a class that extends HTMLElement (or another built-in element class).

basic-custom-element.html

HTML
<script>
  class HelloBanner extends HTMLElement {
    connectedCallback() {
      this.textContent = 'Hello from a custom element!'
      this.style.display = 'block'
      this.style.padding = '12px'
      this.style.background = '#eef6ff'
    }
  }

  customElements.define('hello-banner', HelloBanner)
</script>

<hello-banner></hello-banner>
Rendered as a styled block:
"Hello from a custom element!"
The Hyphen Naming Rule

Every custom element's tag name must contain a hyphen (e.g. my-widget, user-card, x-tabs). This is a permanent rule in the HTML spec, not a convention — it guarantees that no future built-in HTML element will ever collide with a name you choose, since native tags never contain a hyphen.

Name

Valid?

Why

user-card

Yes

Contains a hyphen

fancy-button

Yes

Contains a hyphen

UserCard

No

No hyphen — invalid custom element name

div

No

Reserved built-in element name

No hyphen, no registration
Calling customElements.define('widget', ...) (no hyphen) throws a DOMException at runtime. The hyphen requirement is enforced by the browser, not just a style guide.
Autonomous vs Customized Built-in Elements

There are two flavors of custom element.

Kind

Extends

Used as

Autonomous custom element

HTMLElement directly

Its own brand-new tag, e.g. <user-card>

Customized built-in element

A specific built-in class, e.g. HTMLButtonElement

An existing tag plus an is attribute, e.g. <button is="fancy-button">

customized-builtin.html

HTML
<script>
  class FancyButton extends HTMLButtonElement {
    connectedCallback() {
      this.style.borderRadius = '999px'
      this.style.padding = '8px 20px'
    }
  }

  customElements.define('fancy-button', FancyButton, { extends: 'button' })
</script>

<button is="fancy-button">Click me</button>
Browser support varies
Customized built-in elements (the is="..." form) are not supported in every browser (notably Safari has historically lagged here). Autonomous custom elements have broad, solid support across modern browsers.
Lifecycle Callbacks (Conceptual Overview)

A custom element class can implement several special methods that the browser calls automatically at key moments. You never call these yourself — the browser invokes them for you.

Callback

Called when

connectedCallback()

The element is inserted into the document (can fire more than once if moved)

disconnectedCallback()

The element is removed from the document

attributeChangedCallback(name, oldValue, newValue)

One of the attributes listed in observedAttributes changes

adoptedCallback()

The element is moved into a new document (e.g. via document.adoptNode) — rare in practice

lifecycle-callbacks.js

JS
class StatusPill extends HTMLElement {
  static get observedAttributes() {
    return ['status']
  }

  connectedCallback() {
    console.log('Added to the page')
    this.render()
  }

  disconnectedCallback() {
    console.log('Removed from the page')
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`${name} changed from ${oldValue} to ${newValue}`)
    this.render()
  }

  render() {
    this.textContent = this.getAttribute('status') || 'unknown'
  }
}

customElements.define('status-pill', StatusPill)
Do setup work in connectedCallback, not the constructor
The constructor runs before the element is attached to anything and has restrictions (no attributes or children access yet). connectedCallback is the right place to read attributes, render content, and set up event listeners.
Putting the Pieces Together

Custom Elements rarely stand alone. In real Web Components they are typically combined with a <template> for markup, an attached Shadow DOM for encapsulation, and <slot> for content projection.

  • Custom Elements — define new tag names backed by a class.

  • <template> — hold the reusable, inert markup for the element.

  • Shadow DOM — encapsulate that markup and its styles from the page.

  • <slot> — let consumers project their own content into the encapsulated tree.

Quick Reference

Concept

Detail

Register

customElements.define(name, ClassName)

Naming rule

Tag name must contain a hyphen

Base class

HTMLElement (autonomous) or a built-in class (customized built-in)

Key lifecycle hook

connectedCallback() — runs on insertion

Watch attribute changes

Declare static observedAttributes, implement attributeChangedCallback