{"id":383422,"date":"2025-03-14T06:51:59","date_gmt":"2025-03-14T12:51:59","guid":{"rendered":"https:\/\/css-tricks.com\/?p=383422"},"modified":"2025-03-14T06:52:02","modified_gmt":"2025-03-14T12:52:02","slug":"web-components-demystified","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/web-components-demystified\/","title":{"rendered":"Web Components Demystified"},"content":{"rendered":"\n
Scott Jehl released a course called Web Components Demystified<\/a><\/em>. I love that name because it says what the course is about right on the tin: you’re going to learn about web components and clear up any confusion you may already have about them.<\/p>\n\n\n\n And there’s plenty of confusion to go around! “Components” is already a loaded term that’s come to mean everything from a piece of UI, like a search component, to an element you can drop in and reuse anywhere, such as a React component. The web is chock-full of components, tell you what.<\/p>\n\n\n\n But what we’re talking about here is a set of standards where HTML, CSS, and JavaScript rally together so that we can create custom elements that behave exactly how we want them to. It’s how we can make an element called This is my full set of notes from Scott’s course. I wouldn’t say they’re complete or even a direct one-to-one replacement for watching the course. You’ll still want to do that on your own, and I encourage you to because Scott is an excellent teacher who makes all of this stuff extremely accessible, even to noobs like me.<\/p>\n\n\n\n\n\n\n\n Web components are not built-in elements, even though that’s what they might look like at first glance. Rather, they are a set of technologies that allow us to instruct what the element is and how it behaves. Think of it the same way that “responsive web design” is not a thing but rather a set of strategies for adapting design to different web contexts. So, just as responsive web design is a set of ingredients<\/a> \u2014 including media fluid grids, flexible images, and media queries \u2014 web components are a concoction involving:<\/p>\n\n\n\n These are HTML elements that are not built into the browser. We make them up. They include a letter and a dash.<\/p>\n\n\n\n We’ll go over these in greater detail in the next module.<\/p>\n<\/details>\n\n\n\n Templates are bits of reusable markup that generate more markup. We can hide something until we make use of it. <\/p>\n\n\n\n Much more on this in the third module.<\/p>\n<\/details>\n\n\n\n The DOM is queryable.<\/p>\n\n\n\n The Shadow DOM is a fragment of the DOM where markup, scripts, and styles are encapsulated from other DOM elements. We’ll cover this in the fourth module, including how to There used to be a fourth “ingredient” called HTML Imports, but those have been nixed.<\/p>\n\n\n\n In short, web components might be called “components” but they aren\u2019t really components more than technologies. In React, components sort of work like partials. It defines a snippet of HTML that you drop into your code and it outputs in the DOM. Web Components are built off of HTML Elements. They are not replaced when rendered the way they are in JavaScript component frameworks. Web components are quite literally HTML elements and have to obey HTML rules. For example:<\/p>\n\n\n\n We\u2019re generating meaningful HTML up-front rather than rendering it in the browser through the client after the fact. Provide the markup and enhance it! Web components have been around a while now, even if it seems we\u2019re only starting to talk about them now.<\/p>\n\n\n<\/details>\n\n\n First off, custom elements are not built-in HTML elements. We instruct what they are and how they behave. They are named with a dash and at must contain least one letter. All of the following are valid names for custom elements:<\/p>\n\n\n\n Just remember that there are some reserved names for MathML and SVG elements, like Since custom elements are not built-in elements, they are undefined by default \u2014 and being undefined can be a useful thing! That means we can use them as containers with default properties. For example, they are Working with JavaScript. If there is one This defines and registers the custom element. It teaches the browser that this is an instance of the Custom Elements API and extends the same class that makes other HTML elements valid HTML elements:<\/p>\n\n\n\n Check out the methods we get immediate access to:<\/p>\n\n\n\n It\u2019s possible to define a custom element by extending a specific HTML element. The specification documents this, but Scott is focusing on the primary way.<\/p>\n\n\n\n Scott says do not use this because WebKit is not going to implement it. We would have to polyfill it forever, or as long as WebKit holds out. Consider it a dead end.<\/p>\n\n\n A component has various moments in its \u201clife\u201d span:<\/p>\n\n\n\n We can hook into these to define the element\u2019s behavior.<\/p>\n\n\n\n “When the constructor is called, do this\u2026” We don\u2019t have to have a constructor when working with custom elements, but if we do, then we need to call Constructor is useful, but not for a lot of things. It\u2019s useful for setting up initial state, registering default properties, adding event listeners, and even creating Shadow DOM (which Scott will get into in a later module). For example, we are unable to sniff out whether or not the custom element is in another element because we don\u2019t know anything about its parent container yet (that\u2019s where other lifecycle methods come into play) \u2014 we\u2019ve merely defined it.<\/p>\n\n\n Note that there is some strangeness when it comes to timing things. Sometimes If the This is useful when the component needs to be cleaned up, perhaps like stopping an animation or preventing memory links.<\/p>\n\n\n This is when the component is adopted by another document or page. Say you have some iframes on a page and move a custom element from the page into an iframe, then it would be adopted in that scenario. It would be created, then added, then removed, then adopted, then added again. That\u2019s a full lifecycle! This callback is adopted automatically simply by picking it up and dragging it between documents in the DOM.<\/p>\n\n\n Unlike React, HTML attributes are strings (not props!). Global attributes work as you\u2019d expect, though some global attributes are reflected as properties. You can make any attribute do that if you want, just be sure to use care and caution when naming because, well, we don’t want any conflicts.<\/p>\n\n\n\n Avoid standard attributes on a custom element as well, as that can be confusing particularly when handing a component to another developer. Example: using type as an attribute which is also used by Here’s a quick example showing how to get a Another example, this time showing a callback for when the attribute has changed, which prints it in the element’s contents:<\/p>\n\n\n\n A few more custom element methods:<\/p>\n\n\n\n Custom methods and events:<\/p>\n\n\n\n Bring your own base class, in the same way web components frameworks like Lit do:<\/p>\n\n\n\n Create a custom HTML element called <tasty-pizza><\/code> and the browser knows what to do with it.<\/p>\n\n\n\n\n
Chapter 1: What Web Components Are… and Aren’t<\/h2>\n <\/summary>\n \n\n
Custom elements<\/summary>\n
<my-fancy-heading>\n Hey, I'm Fancy\n<\/my-fancy-heading><\/code><\/pre>\n\n\n\nHTML templates<\/summary>\n
<template>\n <li class=\"user\">\n <h2 class=\"name\"><\/h2>\n <p class=\"bio\"><\/p>\n <\/li>\n<\/template><\/code><\/pre>\n\n\n\nShadow DOM<\/summary>\n
document.querySelector(\"h1\");\n\/\/ <h1>Hello, World<\/h1><\/code><\/pre>\n\n\n\n<slot><\/code> content.<\/p>\n<\/details>\n\n\n\n<!-- Nope -->\n<ul>\n <my-list-item><\/my-list-item>\n <!-- etc. -->\n<\/ul>\n\n<!-- Yep -->\n<ul>\n <li>\n <my-list-item><\/my-list-item>\n <\/li>\n<\/ul><\/code><\/pre>\n\n\n\n\n
Chapter 2: Custom Elements<\/h2>\n <\/summary>\n \n\n
\n
<super-component><\/code><\/li>\n\n\n\n<a-><\/code><\/li>\n\n\n\n<a-4-><\/code><\/li>\n\n\n\n<card-10.0.1><\/code><\/li>\n\n\n\n<card-♠️><\/code><\/li>\n<\/ul>\n\n\n\n<font-face><\/code>. Also, they cannot be void elements, e.g. <my-element \/><\/code>, meaning they have to have a correspoonding closing tag.<\/p>\n\n\n\ndisplay: inline<\/code> by default and inherit the current font-family<\/code>, which can be useful to pass down to the contents. We can also use them as styling hooks since they can be selected in CSS. Or maybe they can be used for accessibility hints. The bottom line is that they do not require JavaScript in order to make them immediately useful.<\/p>\n\n\n\n<my-button><\/code> on the page, we can query it and set a click handler on it with an event listener. But if we were to insert more instances on the page later, we would need to query it when it\u2019s appended and re-run the function since it is not part of the original document rendering.<\/p>\n\n\nDefining a custom element<\/h3>\n\n\n
<my-element>My Element<\/my-element>\n\n<script>\n customElements.define(\"my-element\", class extends HTMLElement {});\n<\/script><\/code><\/pre>\n\n\n\n
<\/figure>\n\n\nBreaking down the syntax<\/h3>\n\n\n
customElements\n .define(\n \"my-element\",\n class extends HTMLElement {}\n );\n\t\n\/\/ Functionally the same as:\nclass MyElement extends HTMLElement {}\ncustomElements.define(\"my-element\", MyElement);\nexport default myElement\n\n\/\/ ...which makes it importable by other elements:\nimport MyElement from '.\/MyElement.js';\nconst myElement = new MyElement();\ndocument.body.appendChild(myElement);\n\n\/\/ <body>\n\/\/ <my-element><\/my-element>\n\/\/ <\/body>\n\n\/\/ Or simply pull it into a page\n\/\/ Don't need to `export default` but it doesn't hurt to leave it\n\/\/ <my-element>My Element<\/my-element>\n\/\/ <script type=\"module\" src=\"my-element.js\"><\/script><\/code><\/pre>\n\n\n\nclass WordCount extends HTMLParagraphElement\ncustomElements.define(\"word-count\", WordCount, { extends: \"p\" });\n\n\/\/ <p is=\"word-count\">This is a custom paragraph!<\/p><\/code><\/pre>\n\n\n\nThe lifecycle<\/h3>\n\n\n
\n
constructor<\/code>)<\/li>\n\n\n\nconnectedCallback<\/code>)<\/li>\n\n\n\nadoptedCallback<\/code>)<\/li>\n\n\n\nattributeChangedCallback<\/code>)<\/li>\n\n\n\ndisconnectedCallback<\/code>)<\/li>\n<\/ul>\n\n\n\nclass myElement extends HTMLElement {\n constructor() {}\n connectedCallback() {}\n adoptedCallback() {}\n attributeChangedCallback() {}\n disconnectedCallback() {}\n}\n\ncustomElements.define(\"my-element\", MyElement);<\/code><\/pre>\n\n\nconstructor()<\/code><\/h4>\n\n\nclass myElement extends HTMLElement {\n constructor() {\n \/\/ provides us with the `this` keyword\n super()\n \n \/\/ add a property\n this.someProperty = \"Some value goes here\";\n \/\/ add event listener\n this.addEventListener(\"click\", () => {});\n }\n}\n\ncustomElements.define(\"my-element\", MyElement);<\/code><\/pre>\n\n\n\nsuper()<\/code> because we\u2019re extending another class and we\u2019ll get all of those properties.<\/p>\n\n\n\nconnectedCallback()<\/code><\/h4>\n\n\nclass myElement extends HTMLElement {\n \/\/ the constructor is unnecessary in this example but doesn't hurt.\n constructor() {\n super()\n }\n \/\/ let me know when my element has been found on the page.\n connectedCallback() {\n console.log(`${this.nodeName} was added to the page.`);\n }\n}\n\ncustomElements.define(\"my-element\", MyElement);<\/code><\/pre>\n\n\n\nisConnected<\/code> returns true<\/code> during the constructor. connectedCallback()<\/code> is our best way to know when the component is found on the page. This is the moment it is connected to the DOM. Use it to attach event listeners.<\/p>\n\n\n\n<script><\/code> tag comes before the DOM is parsed, then it might not recognize childNodes<\/code>. This is not an uncommon situation. But if we add type=\"module\"<\/code> to the <script><\/code>, then the script is deferred and we get the child nodes. Using setTimeout<\/code> can also work, but it looks a little gross.<\/p>\n\n\ndisconnectedCallback<\/code><\/h4>\n\n\nclass myElement extends HTMLElement {\n \/\/ let me know when my element has been found on the page.\n disconnectedCallback() {\n console.log(`${this.nodeName} was removed from the page.`);\n }\n}\n\ncustomElements.define(\"my-element\", MyElement);<\/code><\/pre>\n\n\n\nadoptedCallback()<\/code><\/h4>\n\n\nCustom elements and attributes<\/h3>\n\n\n
<\/figure>\n\n\n\n<input><\/code> elements. We could say data-type<\/code> instead. (Remember that Chris has a comprehensive guide on using data attributes<\/a>.)<\/p>\n\n\nExamples<\/h3>\n\n\n
greeting<\/code> attribute and set it on the custom element:<\/p>\n\n\n\nclass MyElement extends HTMLElement {\n get greeting() {\n return this.getAttribute('greeting');\n \/\/ return this.hasAttribute('greeting');\n }\n set greeting(val) {\n if(val) {\n this.setAttribute('greeting', val);\n \/\/ this setAttribute('greeting', '');\n } else {\n this.removeAttribute('greeting');\n }\n }\n}\ncustomElements.define(\"my-element\", MyElement);<\/code><\/pre>\n\n\n\n<my-element greeting=\"hello\">hello<\/my-element>\n\n<!-- Change text greeting when attribite greeting changes -->\n<script>\n class MyElement extends HTMLElement {\n static observedAttributes = [\"greeting\"];\n \n attributeChangedCallback(name, oldValue, newValue) {\n if (name === 'greeting' && oldValue && oldValue !== newValue) {\n console.log(name + \" changed\");\n this.textContent = newValue;\n }\n }\n }\n \n customElements.define(\"my-element\", MyElement);\n<\/script><\/code><\/pre>\n\n\n\ncustomElements.get('my-element');\n\/\/ returns MyElement Class\n\ncustomElements.getName(MyElement);\n\/\/ returns 'my-element'\n\ncustomElements.whenDefined(\"my-element\");\n\/\/ waits for custom element to be defined\n\nconst el = document.createElement(\"spider-man\");\nclass SpiderMan extends HTMLElement {\n constructor() {\n super();\n console.log(\"constructor!!\");\n }\n}\ncustomElements.define(\"spider-man\", SpiderMan);\n\ncustomElements.upgrade(el);\n\/\/ returns \"constructor!!\"<\/code><\/pre>\n\n\n\n<my-element><button>My Element<\/button><\/my-element>\n\n<script>\n customElements.define(\"my-element\", class extends HTMLElement {\n connectedCallback() {\n const btn = this.firstElementChild;\n btn.addEventListener(\"click\", this.handleClick)\n }\n handleClick() {\n console.log(this);\n }\n });\n<\/script><\/code><\/pre>\n\n\n\nclass BaseElement extends HTMLElement {\n $ = this.querySelector;\n}\n\/\/ extend the base, use its helper\nclass myElement extends BaseElement {\n firstLi = this.$(\"li\");\n}<\/code><\/pre>\n\n\nPractice prompt<\/h3>\n\n\n
<say-hi><\/code> that displays the text “Hi, World!” when added to the page:<\/p>\n\n\n\n