Codeskill

Learn to code, step by step

Web Components and custom elements (HTML side)

Web Components from the HTML side: custom elements, their lifecycle hooks, and how to define tags that behave like first-class platform elements. We are not building a full component framework – we are learning what the browser gives you natively.

What a custom element is

A custom element is a JavaScript class registered with a tag name that must contain a hyphen. Once registered, you use it in HTML like any other tag:

<codeskill-notice variant="warning">
  Your session expires in five minutes.
</codeskill-notice>

The hyphen avoids collisions with future native elements (<button> is taken; <app-button> is yours). Treat the tag name as part of your public API – renaming later is painful.

Defining and registering

class CodeskillNotice extends HTMLElement {
  static get observedAttributes() {
    return ['variant'];
  }

  connectedCallback() {
    this.setAttribute('role', 'status');
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) this.render();
  }

  render() {
    const variant = this.getAttribute('variant') || 'info';
    this.innerHTML = `
      <div class="notice notice--${variant}">
        <slot></slot>
      </div>
    `;
  }
}

customElements.define('codeskill-notice', CodeskillNotice);

connectedCallback runs when the element enters the document. observedAttributes plus attributeChangedCallback react to attribute changes. Use semantic roles in markup where the element conveys status or structure.

Autonomous vs customised built-ins

Most custom elements are autonomous – new tags with new behaviour. You can also extend native elements (customised built-ins) when you want to keep native semantics:

class PrimaryButton extends HTMLButtonElement {
  connectedCallback() {
    this.classList.add('btn', 'btn--primary');
  }
}

customElements.define('primary-button', PrimaryButton, { extends: 'button' });
<button is="primary-button" type="submit">Save</button>

Customised built-ins have patchy support in some browsers for certain elements. Autonomous elements with careful ARIA and keyboard behaviour are the common choice. Prefer extending HTMLButtonElement over a <div role="button"> when you need a button.

Attributes vs properties

HTML attributes are strings; JavaScript properties can be any type. For public APIs, reflect important attributes so markup stays readable in DevTools and SSR output:

set open(value) {
  if (value) {
    this.setAttribute('open', '');
  } else {
    this.removeAttribute('open');
  }
}

get open() {
  return this.hasAttribute('open');
}

HTML contract for components

Document which attributes your element supports, what it renders, and what events it fires. Use slots for user-provided content (covered in the Shadow DOM lesson). Avoid leaking implementation markup into light DOM unless that is intentional. Custom elements work well with progressive enhancement: the tag can exist in HTML before the script loads, then upgrade when defined.

Frameworks often wrap or compile custom elements. Understanding the platform layer means you can integrate with any stack – or skip frameworks entirely for small embeddable widgets.

PreviousSEO technical markup beyond basics