Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

addEventListener
A call on an element that registers a listener for one kind of event.
closest
A way to find the nearest parent of an element that matches a rule you give, starting with the element itself.
closure
A function together with the variables from the place it was written, which stay available when it runs later.
default behaviour
What the browser does on its own when an event happens, such as following a link or submitting a form, before any script intervenes.
DOM
The tree of objects a browser builds from the page, which a script can read and change while the page is open.
element
One item in the page tree, such as a heading or a button, which a script can find and change.
event
Something that happens on the page, such as a click, a key press, or a form being submitted, which a script can respond to.
event bubbling
The way an event rises from the element that received it up through each of its parents.
event target
The element that actually received the action, which may be a child of the element whose listener is running.
function
A reusable block of code that can be called with values and can return an answer to whoever called it.
handler
The function you give to a listener, which the browser runs when the event it waits for happens.
listener
A function registered on an element to wait for one kind of event and run each time that event reaches it.
property
A named value stored on an object, usually read and written with a dot, such as the text of an element.
reference
A value that points at an object rather than holding a copy, so a change made through one is visible through the other.
scope
The part of a program where a name can be used, decided by where it was declared.
variable
A name that holds a value so you can use it again; a constant cannot be pointed at a different value later.

JavaScript from the ground up

An element of your own

Inventing a tag and teaching the browser what it renders: a class, a shadow root, slots, and one line to register it.

Part 10 of 12

Advanced16 min readBack to Courses

A tag you invent

HTML has a fixed vocabulary, and one hole in it: any tag with a hyphen in its name belongs to you. <user-card> is yours; <usercard> is reserved. That single rule is what lets the platform grow without collisions.

Registering the tag takes three things: a class that extends HTMLElement, a name with a hyphen, and one call to customElements.define("user-card", UserCard). From that moment every <user-card> in the document comes to life.

The hyphen is not a style rule. It is a namespace reservation: the HTML parser treats an unknown hyphenated tag as a custom element and lets the registry upgrade it, while a tag without a hyphen is assumed to be a misspelling of something standard. That is why customElements.define("usercard", ...) throws.

The definition

class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" }).innerHTML = `
      <style>h2 { font-size: 1rem; margin: 0 0 4px; }</style>
      <h2><slot name="name">Unknown</slot></h2>
      <p><slot name="role"></slot></p>
    `;
  }
}

customElements.define("user-card", UserCard);

The shadow root

attachShadow({ mode: "open" }) gives the element a private tree of markup and styles. Styles written inside it do not leak out to the page, and the page’s styles do not reach in — which is how a card stays a card wherever it is used.

The constructor runs once, when the element appears in the document. That is where the shadow root is attached and its markup set, ready before the first paint.

A shadow root is not required. An element can render into its ordinary children, and some do. The shadow root earns its place when the component needs styles that must not leak, or markup that the page should not accidentally query and change.

Slots let the page keep its content

A <slot> is a hole the element’s own children pass through. The page writes <user-card><span slot="name">Ada</span></user-card> and the shadow markup decides where that span appears. The words stay in the document — which is why the page still reads sensibly before any script runs.

That is progressive enhancement in one element: the content is HTML, the presentation is the component, and a failed script costs the styling rather than the meaning.

Lifecycle: the browser calls you

A custom element has named moments. The constructor runs once, when the element is created or upgraded. connectedCallback runs each time it is inserted into the document, and disconnectedCallback runs when it leaves. attributeChangedCallback runs when an observed attribute changes — including once for each attribute already present at upgrade.

The split matters because the moments are not interchangeable. The constructor can run before the element’s children have been parsed, so reading them there is unreliable; work that needs the page belongs in connectedCallback. Anything that should react to an attribute needs that attribute named in observedAttributes first.

Attributes are text, and the shadow wall has doors

An HTML attribute is always a string: <status-badge level="warn"> hands over "warn", not a number or an object. The element decides what that string means, and attributeChangedCallback is where the translation happens. Setting the same value again does not fire it, because nothing changed.

The shadow root keeps page styles out, but it is not a sealed box. :host styles the element from inside, ::slotted(...) styles the content passed through a slot, and CSS custom properties cross the boundary in both directions. That is enough to let a page theme a component without reaching into its private tree.

A worked upgrade, in order

Suppose the page contains two <user-card> elements before the script runs. The browser treats them as unknown inline elements and renders their text plainly. When customElements.define runs, each existing element is upgraded: a new instance of the class is created, the constructor attaches a shadow root and fills it, and the element is re-rendered with its slots filled.

Any <user-card> added later is constructed as soon as it is inserted. That is why the definition has to run before the elements are used if you want to avoid a flash of unstyled content, and why the script is often placed in the head with defer rather than at the end of the body.

Three mistakes: using this before super() in the constructor throws; reading children or attributes in the constructor is unreliable because they may not be parsed yet — use connectedCallback; and a name without a hyphen, or one registered twice, fails at customElements.define.

Try these next

  • Add a connectedCallback and a disconnectedCallback that log, then move the element in the page.
  • Observe a second attribute and change it from the console to watch the render update.
  • Style the element from inside with :host and set a custom property from the page.

Teach the tag to render

The page uses user-card twice and the browser renders it as a plain block. Define the element: a shadow root with a heading and a line, and slots so each card’s name and role flow in.

Live Preview
Console output will appear here...

3 left to do

  • The element is registered
  • It renders through a shadow root
  • The content flows through slots

class UserCard extends HTMLElement with a constructor that attaches the shadow root and sets innerHTML — then customElements.define("user-card", UserCard).

A badge that reads its attribute

Define `status-badge` so it renders the word OK when `level="ok"` and Warning when `level="warn"`. Watch the attribute and update the badge when it changes.

Live Preview
Console output will appear here...

3 left to do

  • The level attribute is observed
  • Changes are handled in attributeChangedCallback
  • The element is registered

Give the class a static observedAttributes returning ["level"]; render a span into a shadow root; in attributeChangedCallback, choose the word from the new value.

Keep the words in the page, not in the script. An element that writes its own content is invisible to search engines, unreadable when the script fails, and harder to translate.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Which tag name is yours to define?