cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Web Components

Build reusable custom elements with lifecycle callbacks, Shadow DOM, templates, slots, styling hooks, and events that cross the shadow boundary correctly.

By the end, you can
  • 01
    Define safe custom elementsUse hyphenated names, guard duplicate definitions, and understand lifecycle callbacks.
  • 02
    Build with Shadow DOMChoose open or closed roots, style with host rules, slots, parts, and custom properties.
  • 03
    Handle boundaries honestlyPredict slot fallback, event retargeting, composed events, and realistic styling choices.

Build a new kind of element

Web Components are a set of browser features for building reusable interface pieces without a build step or a framework. You can teach the browser a new tag such as <cf-wc-counter>, give it a class, let it react when it enters or leaves the page, hide its internal markup in a shadow root, and leave openings where page content can show through.

They sit on top of skills from Class basics, Creating & changing elements, Bubbling & capturing, and Custom events. The new idea is the boundary: who owns the DOM, who styles it, and which events cross out.

Real-life analogyA custom element is a new LEGO piece

HTML gives you built-in pieces: buttons, inputs, lists. Custom elements let you add one more piece to the box. Once the browser learns the name, every matching tag on the page is upgraded into your class.

In real life: A new LEGO piece shape
In JavaScript: A new tag name such as cf-wc-counter
In real life: The instruction sheet
In JavaScript: The JavaScript class
In real life: The factory learns that piece once
In JavaScript: customElements.define registers the name once
In real life: That piece snaps in anywhere
In JavaScript: Existing tags upgrade when the definition loads

Where the analogy stops: A LEGO piece is physical and cannot run code. A custom element has JavaScript behavior, lifecycle callbacks, attributes, properties, and events.

Definition

A Web Component is usually a custom element plus optional Shadow DOM, templates, slots, and event conventions that make it reusable on ordinary web pages.

Custom elements and lifecycle

INTERACTIVE

A custom element starts with a class that extends HTMLElement. Its name must start with a lowercase letter and contain a hyphen, such as profile-card. The hyphen is not decoration; it prevents collisions with future built-in HTML elements.

Registering is permanent for the page. If you call customElements.define("cf-wc-counter", Counter) twice, the second call throws a NotSupportedError. Lessons on this site may be revisited through client navigation or re-run by React Strict Mode, so every demo defines only on the client and first checks customElements.get(name).

Define safely on pages with client navigation

Use lesson-prefixed names, guard every definition, and create the element inside an empty ref container React does not manage. React can render the container; the custom element owns the children inside it.

Real-life analogyLifecycle callbacks are tenant notices

The browser sends notices to the element. “You are in the document now.” “You were removed.” “This watched attribute changed.” Your class decides how to update in response.

In real life: A tenant moves in
In JavaScript: connectedCallback runs
In real life: A tenant moves out
In JavaScript: disconnectedCallback runs
In real life: A doorbell wired to one apartment
In JavaScript: attributeChangedCallback for names in observedAttributes

Where the analogy stops: A landlord can notice many things informally. A custom element only gets lifecycle calls the platform defines, and attribute changes only ring the bells you listed.

Try the counter. Set an attribute, set the reflected property, click the shadow button, then read both back. The log shows real constructor, connection, and attribute changes from a browser element.

Component builder: attributes, properties, lifecycle
Counter custom elementPop out in the code editor (opens in a new tab)JavaScript
class CounterBadge extends HTMLElement {  static observedAttributes = ["count", "step"];   set count(value) {    this.setAttribute("count", String(value));  }   connectedCallback() {    this.render();  }   attributeChangedCallback(name, oldValue, newValue) {    if (oldValue !== newValue) this.render();  }} customElements.define("cf-wc-counter", CounterBadge);
Real custom elementcf-wc-counter
    Try it yourself

    Create the element to read attributes and properties.

    The element is defined in a client effect, guarded with customElements.get, then inserted into an empty ref container that React never manages.

    Lifecycle order: create, connect, move, remove

    STEP THROUGH

    Lifecycle order matters because many components fetch data, attach listeners, or measure layout when connected, and clean up when disconnected. The constructor should set up internal state, not assume the element is already in the document. connectedCallback can run more than once for the same element if it is removed and re-appended.

    The replay below is a pure model of the DOM order so it can be tested in Node. We also verified the same callback order in headless Chrome. Change the setting and predict whether the attribute callback appears before or after the first connection.

    Lifecycle lab: create, move, remove, re-append
    Step 0 of 7Ready
    Your turn: follow the blue line

    Choose when the attribute is set, then step through the pure lifecycle model. The order mirrors the browser behavior verified for this lesson.

    Running in
    1. script
    Next: line 1
    Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
    el.setAttribute("count", "2");left.append(el);right.append(el);el.remove();left.append(el);
    CallStoreChangeResultRun = next line. Ran = already executed.
    Recent returnsNothing yet. Start with the blue line.
    When should the observed attribute be set?

    Displayed source currently has 6 lines.

    A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

    Shadow DOM: a boundary for internals

    INTERACTIVE

    Shadow DOM gives an element a private-ish tree for its internals. Page CSS selectors do not normally reach in, and styles inside the shadow root do not leak out. The host still exists in the outer document, so you can place it, set attributes, listen for composed events, and pass custom properties to it.

    Real-life analogyShadow DOM is a snow globe

    A snow globe has a scene inside. Outside dust does not smear across the miniature trees, and the miniature snow does not spill onto your desk. But the globe still sits on the desk and can have knobs you choose to expose.

    In real life: The scene inside the globe
    In JavaScript: Shadow-root internals
    In real life: The glass boundary
    In JavaScript: Style and DOM encapsulation
    In real life: Openings in a picture frame
    In JavaScript: Slots showing host children
    In real life: Knobs the maker exposes
    In JavaScript: ::part() styling hooks

    Where the analogy stops: The glass is not a vault. Closed shadow roots hide element.shadowRoot, but they are not a security boundary and do not protect secrets.

    Toggle open and closed mode. Notice that a page paragraph rule colors the page paragraph but not the shadow paragraph. Then change the custom property: that value is an intentional bridge through the boundary. The demo also includes :host, ::slotted(), and part.

    Shadow style lab: snow globe styles
    Shadow styling shapePop out in the code editor (opens in a new tab)JavaScript
    class NoticeCard extends HTMLElement {  constructor() {    super();    const root = this.attachShadow({ mode: "open" });    root.innerHTML = `<style>      :host { --wc-accent: royalblue; display: block; }      p { color: var(--wc-accent); }      ::slotted(strong) { text-decoration: underline; }      [part="label"] { font-weight: bold; }    </style>    <p part="label">Shadow paragraph</p>    <slot name="title">Fallback title</slot>    <slot>Fallback body</slot>`;  }}
    Real shadow hostopen
    Try it yourself

    Open mode exposes shadowRoot. Page paragraph styles stay outside.

    The actual component uses a CSS custom property, ::slotted(strong), and part=label. Closed mode hides shadowRoot but is not a security boundary.
    Open and closed are API choices

    mode: "open" lets scripts read element.shadowRoot. mode: "closed" makes that property return null. Closed mode can discourage casual coupling, but it is not a security feature.

    Templates and slots

    INTERACTIVE

    A <template> stores inert markup. The browser parses it, but it does not render until you clone template.content. Components use templates to give each instance the same internal structure without rebuilding strings by hand.

    Slots are how light DOM children appear inside a shadow layout. A named slot receives children with the matching slot attribute. The default slot receives the rest. Fallback content appears only when no matching children are assigned. ::slotted() matches only top-level slotted nodes, not grandchildren.

    Template and slot lab: fallback and slotchange
    Template and slotsPop out in the code editor (opens in a new tab)JavaScript
    const template = document.querySelector("#profile-template");const root = host.attachShadow({ mode: "open" });root.append(template.content.cloneNode(true)); root.querySelector("slot[name='title']")  .addEventListener("slotchange", logAssignedNodes); host.innerHTML = `<strong slot="title">Ada</strong><span>Writes tools</span>`;
    Real slot projection2 light children
      Try it yourself

      Pure model says title: Ada | default: Writes tools.

      The component clones a template into its shadow root. The buttons only change light-DOM children on the host element.

      Styling and events across the boundary

      INTERACTIVE

      Boundaries should be deliberate. For styling, use CSS custom properties for theme values, ::part() for named internal pieces, and slots for page-owned content. For events, remember two words: retargeted and composed.

      Real-life analogyA shop answers from its own counter

      Inside the shop, the event starts at a particular button. Outside, the shop counter is the visible source. If the event is not composed, it stays inside the shop.

      In real life: A question starts inside the shop
      In JavaScript: An event fires inside a shadow root
      In real life: The counter answers for the shop
      In JavaScript: Outside listeners see the host as event.target
      In real life: The shop lets the question out
      In JavaScript: composed: true lets a custom event cross

      Where the analogy stops: A real shop has no DOM event path. The analogy is only about the private boundary and whether an event may cross it.

      Event boundary lab: retargeting and composed events
      Event boundaryPop out in the code editor (opens in a new tab)JavaScript
      shadowButton.addEventListener("click", (event) => {  console.log("inside target:", event.target);}); document.addEventListener("click", (event) => {  console.log("outside target:", event.target);  console.log(event.composedPath().map(nodeName));}); shadowButton.dispatchEvent(new CustomEvent("secret", {  bubbles: true,  composed: false,}));
      Real event log0 lines
        Try it yourself

        The custom event bubbles inside the shadow root, then stops at the boundary because composed is false.

        Click is a composed user-interface event in modern browsers. CustomEvent defaults to composed false, so opt in deliberately.

        Now sort common design needs by where they belong. This is the daily architecture question in Web Components: what should the page own, what should the component own, and what should be an explicit bridge?

        Where should this requirement live?
        • The page supplies the card title text
        • A private increment button the component owns
        • A component shows whatever paragraph the page put inside it
        • Let page CSS style only the exposed label knob
        • The actual input value submitted with a form
        • Repeated internal wrapper copied for each instance
        • Default text shown until the page provides children
        • A chosen border element the page may color
        Try it yourself
        0 of 8 correct

        Sort each need into light DOM, Shadow DOM, slot, or part. Every explanation describes the ownership boundary.

        Choose a category for every card. You can change an answer at any time; Reset clears them all.

        Where you will use this

        Web Components shine when the component must survive outside one app framework: design-system buttons used by several teams, documentation widgets embedded in static pages, a map picker that plain HTML pages can use, or a small dashboard card shared between a React page and a server-rendered page.

        They are not magic. If a widget is only used inside one React app, a React component can be simpler. If the page must submit native form controls directly, light DOM may beat Shadow DOM. If outside CSS needs full control, expose a small set of parts rather than letting consumers depend on internal class names.

        Web Component pieces compared
        PieceOwnsMain jobCommon trap
        Custom elementThe host tag and classAdds a new HTML element with lifecycleDefining the same name twice
        Shadow DOMInternal DOM and stylesEncapsulates structure and selectorsTreating closed mode as security
        TemplateReusable inert markupClones structure for each instanceForgetting it does not render by itself
        SlotPlacement of host childrenProjects light-DOM contentExpecting ::slotted() to style grandchildren
        PartNamed internal styling hookLets consumers style chosen internalsExposing too many internals
        Declarative Shadow DOM

        Modern browsers also support declarative Shadow DOM with <template shadowrootmode="open">. Feature-detect before relying on it, for example by checking whether HTMLTemplateElement.prototype has the relevant shadow-root property in the browser you target.

        Common misconceptions

        “A custom element must use Shadow DOM.”

        No. A custom element can render light DOM, use Shadow DOM, or do almost nothing. The features compose but are separate.

        “Closed shadow roots are secure.”

        Closed mode hides element.shadowRoot from normal code. It does not protect secrets, permissions, or business logic.

        “Any attribute change calls attributeChangedCallback.”

        Only names returned from observedAttributes call it. If a name is missing, the callback stays silent.

        “Slots move nodes into the shadow root.”

        Slotted nodes remain light-DOM children of the host. The slot controls where they appear visually.

        “All events leave a shadow root.”

        Most user-interface events, like clicks, are composed. Many others are not. Custom events need composed: true when outside listeners should receive them.

        “Page CSS can style anything with a clever selector.”

        Shadow DOM blocks normal selectors. Components expose intentional styling hooks with custom properties, slots, and parts.

        Practice: build the boundary muscle

        5 EXERCISES
        Exercise 1 · Warm-upName the element

        Decide whether profile-card is a valid custom element name.

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 2 · PracticePredict the lifecycle log

          What is the first callback in the simplified program?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          console.log("constructor");
          console.log("attributeChangedCallback");
          console.log("connectedCallback");

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 3 · PracticeReflect a property

            A count property setter calls setAttribute("count", String(value)). What string is written for value 5?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const oldAttribute = "2";
            const next = 5;
            console.log(String(next));

            Answer, then press Check. Spacing and letter case don’t matter.

              Exercise 4 · PracticeFind the duplicate-define bug

              You see NotSupportedError after navigating away and back. Write the guard you would add around customElements.define.

                Exercise 5 · ChallengeDesign a product card boundary

                Sketch where each piece belongs for a reusable product card: product title, internal button layout, theme color, and a save event.

                  Quiz: check your understanding

                  8 QUESTIONS

                  Answer once, then read every explanation. The wrong answers name the traps professionals still hit.

                  Lesson quiz · 8 questionsScore: first tries count
                  1. Question 1 of 8Which custom element name is valid?

                    Choose an answer to see the explanation.

                  2. Question 2 of 8What happens if the same name is defined twice on one page?

                    Choose an answer to see the explanation.

                  3. Question 3 of 8Which callback reacts to an observed attribute change?

                    Choose an answer to see the explanation.

                  4. Question 4 of 8What does this print when count reflects through setAttribute?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    const oldAttribute = "2";
                    const next = 5;
                    console.log(String(next));

                    Choose an answer to see the explanation.

                  5. Question 5 of 8In closed shadow mode, what does element.shadowRoot return?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    console.log(null);

                    Choose an answer to see the explanation.

                  6. Question 6 of 8What can ::slotted(p span) style?

                    Choose an answer to see the explanation.

                  7. Question 7 of 8What does an outside click listener see for a click inside an open shadow root?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    console.log("cf-wc-event-card");

                    Choose an answer to see the explanation.

                  8. Question 8 of 8Which custom event option lets it leave a shadow root?

                    Choose an answer to see the explanation.

                  Key takeaways

                  • Custom element names need a lowercase start and a hyphen, and definitions are permanent for the page.
                  • Use connectedCallback, disconnectedCallback, and attributeChangedCallback for lifecycle work; observed attributes are opt-in.
                  • Shadow DOM encapsulates internal DOM and styles. Open versus closed changes the API, not security.
                  • Templates clone inert markup; slots project host children; ::part and custom properties expose deliberate styling hooks.
                  • Events crossing a shadow root are retargeted, and custom events need composed: true to cross.

                  Remember the one-liner.
                  A Web Component teaches the browser a reusable tag, owns its internals, and chooses exactly which content, styles, and events cross its boundary.

                  Up next: The iteration protocols.

                  CompleteFrontend Clear concepts. Working examples.