completefrontend
Open lab My progress
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
    Register custom elements safelyPick a valid tag name, register it only once, and know when each lifecycle callback runs.
  • 02
    Build with Shadow DOMChoose open or closed mode, and style the inside with :host, slots, parts, and custom properties.
  • 03
    Predict what crosses the boundaryTell when slot fallback text shows, which target listeners outside see, and when a custom event gets out.

Build a new kind of element

HTML gives you ready-made elements, such as <button> and <input>. Web Components let you make your own. They are features built into the browser, so you need no framework and no build step.

With Web Components you can:

  • Teach the browser a new tag, such as <cf-wc-counter>, and give it a class that says how it behaves.
  • Run your own code when the element is added to the page or taken off it.
  • Hide the element's inside parts in a shadow root. That is a private area that page styles can't reach.
  • Leave openings, called slots, where content from the page shows through.

This lesson uses ideas from Class basics, Creating & changing elements, Bubbling & capturing, and Custom events. The new idea here is the boundary: the line between the element's private inside and the rest of the page. You will learn what stays inside, what the page controls, and what is allowed to cross.

Real-life analogyA custom element is a new LEGO piece

A LEGO box comes with standard pieces. Now imagine you design a new piece and send its instruction sheet to the factory. The factory learns it once. From then on, every piece of that shape is made the way your sheet says.

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 for the piece
In JavaScript: The JavaScript class that says how the element behaves
In real life: The factory learns the piece once
In JavaScript: customElements.define registers the name once
In real life: Every piece of that shape follows the sheet
In JavaScript: Every matching tag on the page becomes your element
Definition

A Web Component is a reusable element you make yourself. It is built from a custom element: a new tag name joined to a class. It can also use a shadow root, templates, and slots.

Custom elements and their lifecycle

INTERACTIVE

A custom element is a new HTML tag that you define. You make one in two steps:

  1. Write a class that extends HTMLElement. HTMLElement is the class behind every ordinary HTML element, so your class gets everything they can do.
  2. Register a tag name for it with customElements.define(name, Class). customElements is the browser's registry: its list of known custom tag names.

The tag name has three rules:

  • It starts with a lowercase letter.
  • It has no capital letters.
  • It contains a hyphen (-), such as profile-card.

The hyphen keeps your names apart from built-in tags. No new built-in HTML tag will ever have a hyphen, so a future tag can never take your name.

Each name can be registered only once per page. If you call customElements.define("cf-wc-counter", Counter) a second time, it throws a NotSupportedError. On this site, a lesson's code can run more than once. That happens when you leave a lesson and come back without a full reload. It also happens in React's Strict Mode, which runs setup code twice while the site is being built. So every demo here does two things:

  • It registers elements only in the browser, never on the server.
  • It first calls customElements.get(name). That gives back the class if the name is already taken, or undefined if the name is free.
Register safely when code can run twice

Give each lesson's tags a name start, like cf-wc-. Check the name with customElements.get before every define. Then make the element inside an empty container that React leaves alone. (React reaches that container through a ref, its way to hold a real page element.) React draws the container. Your custom element controls everything inside it.

Mistake: registering the same name twicePop out in the code editor (opens in a new tab)JavaScript
class ProfileCard extends HTMLElement {}
customElements.define("cf-wc-profile-card", ProfileCard);
customElements.define("cf-wc-profile-card", ProfileCard);

Line 2 registers the name. Line 3 tries to register it again. The registry does not swap in the new class. It throws NotSupportedError instead. That is why code that can run twice needs a check first.

A tag already on the page is upgraded laterPop out in the code editor (opens in a new tab)JavaScript
const card = document.createElement("cf-wc-upgrade-demo");
console.log(card instanceof HTMLElement);
document.body.append(card);
class UserCard extends HTMLElement {}
customElements.define("cf-wc-upgrade-demo", UserCard);
console.log(card instanceof UserCard);

Line 1 makes an element called cf-wc-upgrade-demo. No class is registered for that name yet, so it is a plain element. Line 2 prints true: it is an HTMLElement. Line 3 puts it on the page. Line 4 writes the class, and line 5 registers it. The browser now finds the tag already on the page and turns it into a UserCard. This is called an upgrade. Line 6 prints true.

Left, before define: the variable card holds a plain HTMLElement. An arrow labeled define() leads right. After define: the same card now holds a UserCard.before defineHTMLElementcardplain elementdefine()after defineUserCardcardupgraded
The tag is on the page before its class exists. When define() runs, the browser upgrades that same element into a UserCard.

An element that is not on the page is not upgraded by itself. If you need it upgraded right away, call customElements.upgrade(element).

Sometimes your code must wait until a tag's class is registered. For example, it may want to call a method that only the class has. customElements.whenDefined("profile-card") gives you a promise: an object that settles later, here when the name is registered. It only waits. It never registers the class for you.

Wait until a name is registeredPop out in the code editor (opens in a new tab)JavaScript
const name = "cf-wc-ready-badge";
const ready = customElements.whenDefined(name);
console.log(ready instanceof Promise);
class ReadyBadge extends HTMLElement {}
customElements.define(name, ReadyBadge);
console.log(customElements.get(name) === ReadyBadge);

Line 2 starts waiting before the class even exists. Line 3 prints true, because ready is a promise. Line 5 registers the class, and that settles the promise. Line 6 prints true: the registry now gives back ReadyBadge for the name. Waiting doesn't make a second define safe, so you still need the check from before.

The browser also calls special methods on your element at key moments in its life. They are called lifecycle callbacks:

  • The constructor runs when the element is created.
  • connectedCallback runs each time the element is added to the page.
  • disconnectedCallback runs each time the element is taken off the page.
  • attributeChangedCallback runs when a watched attribute changes. You list the attributes to watch in static observedAttributes. A static field belongs to the class itself, not to each element.
Real-life analogyLifecycle callbacks are notices to a tenant

A building manager sends a tenant notices: "You have moved in." "You have moved out." The tenant also hears the doorbells they installed, and only those. The tenant decides what to do about each notice.

In real life: A notice: you have moved in
In JavaScript: connectedCallback runs: the element was added to the page
In real life: A notice: you have moved out
In JavaScript: disconnectedCallback runs: the element was taken off the page
In real life: A doorbell the tenant chose to install
In JavaScript: attributeChangedCallback runs, but only for names in observedAttributes

Elements have attributes and properties. An attribute is written in HTML, like count="2", and it always holds text. A property is a value on the JavaScript object, like element.count, and it can be any type. The counter below reflects its property: setting element.count also writes the count attribute.

Try the counter. It is a real custom element that watches count and step. It starts with count="2" and step="3". Use the buttons to:

  • set the count attribute to 10;
  • set the count property to 21;
  • read both back.

You can also click the counter's own button. It adds the step, 3, to the count. The log lists each callback that ran.

Counter lab: attributes, properties, and callbacks
The counter element, shortenedPop 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 lab registers the element only in the browser, checks customElements.get first, and puts the element in an empty container that React leaves alone.
    A click updates the counter's numberPop out in the code editor (opens in a new tab)JavaScript
    class ClickCounter extends HTMLElement {
      count = 0;
      connectedCallback() {
        this.innerHTML = "<button>0</button>";
        this.querySelector("button").addEventListener("click", () => {
          this.querySelector("button").textContent = String(++this.count);
        });
      }
    }
    
    customElements.define("cf-wc-click-counter", ClickCounter);
    const counter = document.createElement("cf-wc-click-counter");
    document.body.append(counter);
    counter.querySelector("button").click();
    console.log(counter.querySelector("button").textContent);

    Line 2 starts the count at 0. When the element is added to the page, connectedCallback (line 3) puts a button inside it (line 4). Line 5 listens for clicks. Each click adds 1 to the count and shows the new number (line 6). Line 14 clicks the button once, so line 15 prints 1.

    An attribute is text; a property can be a numberPop out in the code editor (opens in a new tab)JavaScript
    const countAttribute = "2";
    const countProperty = Number(countAttribute);
    console.log(typeof countAttribute, countAttribute);
    console.log(typeof countProperty, countProperty);

    Line 1 holds the count as text, the way an attribute stores it. Line 2 turns that text into a number with Number(). Lines 3 and 4 print the type and the value of each: string 2, then number 2. If a property and an attribute describe the same thing, keep them in step. Turn the text into a number where your component reads it, and check that the result really is a number.

    On/off attributes only check whether they are therePop out in the code editor (opens in a new tab)JavaScript
    const button = document.createElement("button");
    button.setAttribute("disabled", "false");
    console.log(button.disabled);
    button.removeAttribute("disabled");
    console.log(button.disabled);

    Some attributes, like disabled, are on/off switches. For these, only one thing matters: is the attribute there or not? Line 2 sets disabled to the text "false". The attribute is still there, so line 3 prints true. Line 4 removes it, so line 5 prints false.

    The order of lifecycle callbacks

    STEP THROUGH

    The order matters. Many components start work when they are added to the page: they load data, add listeners, or measure their size. They clean that work up when they are taken off. So:

    • The constructor should only set up the element's own starting values. The element is not on the page yet.
    • connectedCallback can run more than once for the same element. It runs again each time the element is moved, or taken off and added back.

    The step-through below copies the order a real browser uses. It is a model written in plain JavaScript, so the site's tests can check it. The same order was also checked in Chrome. Pick a setting, then guess: does the attribute callback come before or after the first connectedCallback?

    Lifecycle lab: create, add, move, remove, add again
    Step 0 of 7Ready
    Your turn: follow the blue line

    Choose when the attribute is set, then step through. This model follows the order a real browser uses, and the same order was checked in Chrome.

    Running in
    1. script
    Next: line 1
    el.setAttribute("count", "2");left.append(el);right.append(el);el.remove();left.append(el);
    CallStoreChangeResultBlue line: runs next. Ran: already executed.
    Recent returnsNothing yet. Start with the blue line.
    When is the watched attribute set?

    Watch where attributeChangedCallback lands in the log.

    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.

    Line 4 moves the element from left to right. A move takes the element off the page and then adds it again. So you see disconnectedCallback first, then connectedCallback.

    Shadow DOM: a private inside for your element

    INTERACTIVE

    The DOM is the tree of elements that makes up a page. Shadow DOM gives one element its own small, separate tree. That tree starts at a shadow root. The element that holds the shadow root is called the host. You make one with this.attachShadow({ mode: "open" }).

    The boundary works both ways:

    • CSS rules on the page don't reach the elements inside the shadow root.
    • CSS rules inside the shadow root don't leak out to the page.

    The host itself is still a normal element on the page. You can place it, set its attributes, and listen for its events.

    A page CSS rule, p color red, has an arrow to the page paragraph, which turns red. On the right, a window called shadow root holds its own paragraph. The page rule does not reach it, so it is not red.p { color: red }page <p>turns redshadow rootshadow <p>not red
    The page's p rule colors the page's paragraph. The paragraph inside the shadow root is out of its reach.
    Real-life analogyShadow DOM is a snow globe

    A snow globe has a little scene inside. Dust on your desk can't get onto the tiny trees. The tiny snow can't spill onto your desk. Still, the globe sits on your desk like any other object. Its maker can also add knobs on the outside for you to turn.

    In real life: The little scene inside the globe
    In JavaScript: The elements inside the shadow root
    In real life: The glass
    In JavaScript: The boundary that keeps styles in and out
    In real life: Knobs the maker adds on the outside
    In JavaScript: The styling hooks the component offers, like ::part() and CSS custom properties

    Try the lab below. It shows a page paragraph and a component with its own shadow paragraph. The page has a CSS rule that colors its paragraphs red.

    • The rule colors the page paragraph, but not the shadow paragraph.
    • Switch between open and closed mode. Watch what element.shadowRoot gives back.
    • Switch the accent color. It is a CSS custom property, a variable in CSS written like --wc-accent. Custom properties pass through the boundary, so the page can change the shadow text color on purpose.

    The lab's code also uses :host, ::slotted(), and part. You will meet each one below.

    Shadow style lab: what page CSS can reach
    How the shadow card is styledPop 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: element.shadowRoot gives the shadow root. The page's paragraph rule stays outside.

    The component uses a CSS custom property, ::slotted(strong), and part="label". Closed mode hides shadowRoot, but it is not a security feature.
    Let the page style one inside buttonCSS
    :host {
      display: block;
      --wc-accent: royalblue;
    }
    
    cf-wc-counter::part(button) {
      border-color: var(--wc-accent);
    }

    This CSS shows two tools:

    • :host (lines 1 to 4) is written inside the shadow root. It styles the host element itself.
    • ::part(button) (lines 6 to 8) is written on the page. It styles an inside element that the component marked with part="button".

    Page CSS can't reach that button any other way. Only add a part name if you are happy to keep it working for everyone who uses your component.

    Open or closed is not about security

    mode: "open" lets any script read element.shadowRoot. mode: "closed" makes that property give null. Closed mode can stop other code from using your inside parts by accident. It does not keep anything secret.

    Templates and slots

    INTERACTIVE

    A <template> element holds HTML that the browser reads but does not show. Nothing inside it appears or runs. To use it, you copy its contents with template.content.cloneNode(true). Components copy one template into each new element, so every element of that tag gets the same inside parts.

    A slot is an opening in the shadow tree. The host's own children, the ones the page wrote between the host's tags, show up through it. Those children are called the light DOM: they belong to the page, not to the shadow root.

    • A named slot, like <slot name="title">, shows the children that have a matching slot="title" attribute.
    • The default slot, a <slot> with no name, shows all the other children.
    • Text written inside a slot is fallback text. It shows only when no child goes to that slot.
    The page fills a user card's slotsHTML
    <cf-wc-user-card>
      <strong slot="name">Asha</strong>
      <span>Writes tools</span>
    </cf-wc-user-card>

    The strong element has slot="name", so it shows in the slot named name. The span has no slot name, so it shows in the default slot. This is only the page's side of the HTML, not a whole component, so there is nothing to run.

    Left, from the page: a card Asha with slot="name", and a card Writes tools with no slot name. Arrows send Asha to the name slot and Writes tools to the default slot inside the cf-wc-user-card window.from the pageAshaslot="name"Writes toolsno slot namecf-wc-user-cardname slotdefault slot
    The page's children fill the card's openings. Asha asks for the name slot. Writes tools has no slot name, so it goes to the default slot.

    Slotted children are not moved into the shadow root. They stay children of the host. The slot only decides where they appear. To style them from inside the component, use ::slotted(). It only matches the slotted children themselves, not elements nested inside them.

    In the lab, switch the title child and the body child on and off. Watch the fallback text appear when a slot is empty. The slotchange event fires on a slot each time the children shown in it change.

    Slot lab: fallback text and slotchange
    Copy a template, then fill its 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 slots2 host children
      Try it yourself

      Expected: title: Ada | default: Writes tools.

      The component copies a template into its shadow root. The buttons only add or remove the host's own children, in the light DOM.

      Styling and events across the boundary

      INTERACTIVE

      Every crossing of the boundary should be on purpose. For styling, a component has three tools:

      • CSS custom properties, for theme values like a brand color;
      • ::part(), for named inside elements;
      • slots, for content the page owns.

      For events, two words matter:

      • Retargeted: when an event leaves a shadow root, listeners outside see the host as event.target, not the inside element. (event.target is the element where the event started.)
      • Composed: an event with composed: true is allowed to cross the boundary. Without it, the event stays inside the shadow root.
      Real-life analogyA shop answers from its own counter

      Inside a shop, a question starts with one worker. People outside never see that worker. To them, the answer comes from the shop's front counter. Some questions the shop keeps inside, and they never reach the street at all.

      In real life: A question starts with one worker inside the shop
      In JavaScript: An event starts on an element inside a shadow root
      In real life: People outside only see the front counter answer
      In JavaScript: Listeners outside see the host as event.target
      In real life: The shop decides to pass the question outside
      In JavaScript: composed: true lets a custom event cross

      Try the lab. Click the button inside the shadow root, and compare the target seen inside with the target seen outside. Then send a custom event with composed set to false, then to true, and see whether the page hears it.

      Event lab: which target, and does it get out?
      One event, two listenersPop out in the code editor (opens in a new tab)JavaScript
      shadowButton.addEventListener("secret", (event) => {  console.log("inside target:", event.target.localName);}); document.addEventListener("secret", (event) => {  console.log("outside target:", event.target.localName);}); shadowButton.dispatchEvent(new CustomEvent("secret", {  bubbles: true,  composed: true,}));
      Real event log0 lines
        Try it yourself

        composed is false, so the custom event moves up inside the shadow root and stops at the boundary.

        Clicks are composed in modern browsers. A CustomEvent is not composed unless you set composed: true.
        A composed custom event reaches both sidesPop out in the code editor (opens in a new tab)JavaScript
        shadowButton.addEventListener("secret", (event) => {
          console.log("inside target:", event.target.localName);
        });
        
        document.addEventListener("secret", (event) => {
          console.log("outside target:", event.target.localName);
        });
        
        shadowButton.dispatchEvent(new CustomEvent("secret", {
          bubbles: true,
          composed: true,
        }));

        Here the button lives in the shadow root of a div host. Line 9 sends a custom event called secret from the button. Line 10 lets it bubble, which means move up from the button to its parents. Line 11 lets it cross the boundary. Both listeners run:

        • The inside listener (line 1) sees the real button, so it prints inside target: button.
        • The outside listener (line 5) sees the host, so it prints outside target: div.
        A box called div, the host, holds a button card inside it. On the left, the inside listener points at the button: target is button. On the right, the outside listener points at the div box: target is div.inside listenertarget: buttonbuttondivthe hostoutside listenertarget: div
        One event, two views. The listener inside the shadow root sees the button. The listener on document only sees the host, a div.

        Leave out composed, or set it to false, when the event should stay private.

        Now sort some common needs by where they belong. Ask the same question each time: should the page own this, should the component own it, or should it be a planned bridge between them?

        Where does each need belong?
        • The card's title text, written by the page
        • A private +1 button that the component owns
        • A spot that shows whatever paragraph the page puts inside the component
        • One label that page CSS may style, and nothing else
        • The input whose value a form sends
        • An inside wrapper copied into each new card
        • Default text shown until the page gives a child
        • One border element the page may color
        Try it yourself
        0 of 8 correct

        Sort each need into light DOM, Shadow DOM, slot, or part. Each explanation says who owns it.

        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 work best when one piece of a page must work in many places, whatever framework each page uses. For example:

        • buttons from a company's design system (its shared set of styled parts), used by several teams;
        • help widgets added to plain HTML pages;
        • a map picker that plain HTML pages can use;
        • a small dashboard card shared by a React page and a page built on the server.

        They are not always the best choice:

        • If a widget is only used inside one React app, a React component is often simpler.
        • If a form must send its own input fields directly, keeping them in the light DOM may be easier than Shadow DOM.
        • If page CSS needs to style many inside parts, offer a small set of parts. Don't let users depend on your inside class names.

        The table below puts the five pieces side by side. Read each row as: this piece is in charge of this, it does this job, and here is a common mistake.

        The five Web Component pieces side by side
        PieceWhat it is in charge ofWhat it doesA common mistake
        Custom elementThe new tag and its class.It adds a new HTML element with lifecycle callbacks.Registering the same name twice.
        Shadow DOMThe element's inside parts and their styles.It keeps page CSS out and inside CSS in.Thinking closed mode keeps secrets.
        TemplateHTML that is ready to copy.It gives each element a copy of the same inside parts.Expecting it to show up by itself.
        SlotWhere the host's children appear.It shows the page's content inside the component.Expecting ::slotted() to style elements nested inside the slotted ones.
        PartOne named inside element.It lets page CSS style that element.Marking too many inside elements as parts.
        Shadow DOM written in HTML

        Modern browsers can also make a shadow root straight from HTML, with no JavaScript. You write <template shadowrootmode="open"> inside the host. This is called declarative Shadow DOM. Check that the browser supports it before you rely on it. One way is to test whether "shadowRootMode" in HTMLTemplateElement.prototype is true.

        A custom element does not join a form by itself, even with Shadow DOM. To let it send a value with a form:

        1. Set static formAssociated = true on the class.
        2. Check that attachInternals() exists. It gives you an ElementInternals object, which lets the element set its form value.
        3. If it doesn't exist, keep a normal <input> or another fallback.
        Check that form support existsPop out in the code editor (opens in a new tab)JavaScript
        class CheckoutField extends HTMLElement {
          static formAssociated = true;
        }
        
        console.log(CheckoutField.formAssociated);
        console.log(typeof HTMLElement.prototype.attachInternals === "function");

        Line 2 turns form support on for the class. Line 5 prints true. Line 6 prints true when the browser has attachInternals().

        Common misconceptions

        "A custom element must use Shadow DOM."

        No. A custom element can keep its parts in the light DOM, use Shadow DOM, or show nothing at all. These features work well together, but each one also works alone.

        "Closed shadow roots are secure."

        Closed mode only makes element.shadowRoot give null to normal code. It does not protect secrets, permissions, or business rules.

        "Any attribute change runs attributeChangedCallback."

        It only runs for names listed in observedAttributes. If a name is missing from that list, nothing runs.

        "Slots move children into the shadow root."

        Slotted children stay children of the host, in the light DOM. The slot only decides where they show up.

        "All events leave a shadow root."

        Most user events, like clicks, are composed, so they cross. Many other events are not. A custom event needs composed: true if listeners outside should hear it.

        "Page CSS can style anything with a clever selector."

        Shadow DOM blocks normal page selectors. The component chooses what the page may style: custom properties, slots, and parts.

        Practice: where the boundary goes

        5 EXERCISES
        Exercise 1 · Warm-upCheck a tag name

        Is profile-card a valid custom element name? Answer yes or no.

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

          Exercise 2 · PracticePredict the lifecycle log

          This short program prints callback names in the order a real element runs them when you set a watched attribute and then add the element to the page. Which name does it print first?

          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 runs setAttribute("count", String(value)). Which text does it write when the value is 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 · PracticeFix the double-register bug

              You leave the lesson, come back, and see NotSupportedError. Write the check you would add around customElements.define.

                Exercise 5 · ChallengePlan a product card

                Plan a reusable product card. Decide where each of these belongs: the product title, the layout of its inside buttons, the theme color, and a save event.

                  Quiz: check your understanding

                  8 QUESTIONS

                  Answer once, then read every explanation. The wrong answers show mistakes that even experienced developers make.

                  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 registered twice on one page?

                    Choose an answer to see the explanation.

                  3. Question 3 of 8Which callback runs when a watched attribute changes?

                    Choose an answer to see the explanation.

                  4. Question 4 of 8A count setter writes the number 5 with setAttribute. What does this print?

                    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 mode, what does element.shadowRoot give back?

                    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) style?

                    Choose an answer to see the explanation.

                  7. Question 7 of 8In the event lab, what target does a listener on document see for a click inside the 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

                  • A custom element name starts with a lowercase letter and has a hyphen. You can register each name only once per page.
                  • connectedCallback and disconnectedCallback run when the element is added to the page and taken off. attributeChangedCallback runs only for names in observedAttributes.
                  • Shadow DOM keeps an element's inside parts and styles private. Open or closed mode changes what scripts can read. It is not security.
                  • A template holds HTML to copy. Slots show the page's children inside the component. ::part() and custom properties let the page style chosen parts.
                  • Events that leave a shadow root are retargeted to the host. A custom event needs composed: true to leave at all.

                  Remember the one-liner.
                  A Web Component teaches the browser a new tag. It owns its inside parts and chooses which content, styles, and events can cross its boundary.

                  Up next: The iteration protocols.

                  CompleteFrontend Clear concepts. Working examples.