Those familiar with React or any other javascript framework are already aware of the component-based architecture. You break the UI into reusable pieces of code and stitch them together when required. Custom elements in HTML are a way to extend native elements. Javascript frameworks simulate the behavior of components in a web page whereas custom elements provide a native HTML-ly way to do so. A web component uses custom elements along with other techniques, such as the shadow DOM. HTML Types Of Custom Elements # Autonomous custom elements Customized built-in elements Autonomous custom elements extend the generic class. On the other hand, a customized custom element extends a specific HTML element's class and builds on top of existing functionality. For example, if you want a custom anchor element, you can extend the . HTMLElement HTMLAnchorElement Defining Custom Elements # To define a custom element, we need to create a javascript class extending the native HTMLElement class. Try creating the below custom element in a code pen: class Demo extends HTMLElement { constructor() { super() } connectedCallback() { this.textContent = "hello" } } customElements.define("demo", Demo) And then invoking it in HTML: <demo></demo> It won't let you create it. Why? See the error. Uncaught SyntaxError: Failed to execute 'define' on 'CustomElementRegistry': "demo" is not a valid custom element name This is not a bug, this is intentionally done to separate custom elements from native HTML elements. Custom elements must contain a hyphen in their name to make custom elements recognizable and distinct from HTML elements. So now, if you change it to something like and change the class name to , it works. demo-webc DemoWebC class DemoWebC extends HTMLElement { constructor() { super() this.customProperty = "custom" } connectedCallback() { this.textContent = "hello" } } // two arguments: tag name, class name customElements.define("demo-webc", DemoWebC) It is always recommended to call the method first in the constructor as it initializes the default properties of the class by invoking its constructor. super() HTMLElement The method is for detecting when the element is loaded into the page. There's also a method named which detects if the element is removed from the page. A third method name says that the element has moved to a new page. connectedCallback() disconnectedCallback() adoptedCallback() You can define custom properties inside the constructor and use them as custom attributes in your element. constructor() { super() this.customProperty = { name: "data-custom", value: "custom value" } connectedCallback() { this.textContent = "hello" this.setAttribute(this.customAttribute.name, this.customAttribute.value) } } But what if you need to modify the functionality of the attribute's value change? That's where method comes into action. In order to see it in action, you need to first define a static class property and set it to an array of all the attributes you want to keep track of. attributeChangedCallback() observedAttributes The fires if those attributes mentioned in the static property change. Note that if the attribute is already present when the custom element loads, this method is fired at that time, too. attributeChangedCallback() observedAttributes static observedAttributes = ["data-custom"] constructor() { super() } attributeChangedCallback(name, old, newValue) { console.log(name, old, newValue) } Once you are done with building a custom element, you must register it by using the method. It's callable on the global object ( ), which is a registry of custom elements. define() customElements window.customElements customElements.define("custom-element-name", ClassName, options) This was all for defining a customized autonomous custom element. What about extending only an anchor HTML element? For that, instead of extending the class, extend the class. And specify which type of HTML element it extends with the option. HTMLElement HTMLAnchorElement extends class DemoAnchor extends HTMLAnchorElement { constructor() { super() } connectedCallback() { this.textContent = "syntackle.live" this.href = "https://syntackle.live" } } customElements.define("demo-anchor", DemoAnchor, { extends: "a" }) You can't use this element like because it's not an autonomous element, instead you can use it like this: <demo-anchor> <a is="demo-anchor"></a> Web Components # Web components are more than just custom elements. They sometimes also involve a shadow DOM. A DOM, as the name suggests, is a sub-DOM tree for HTML elements. It is mainly used for encapsulation and restricting styles up to the web component only. "shadow" Shadow DOM To create a shadow DOM, attach it to a host, in our case the custom element itself is a host to the shadow DOM. However, the shadow DOM can only be attached to a custom element or . these built-in elements mentioned in the HTML spec You access elements outside the shadow DOM from inside the shadow DOM. can class DemoWebC extends HTMLElement { constructor() { super() } connectedCallback() { const shadow = this.attachShadow({mode: "open"}) const style = document.createElement("style") style.textContent = `p { color: blue; }` shadow.appendChild(style) const text = document.createElement("p") text.textContent = "hello" shadow.appendChild(text) } } customElements.define("demo-webc", DemoWebC) Shadow DOM has two modes: and . Open means external elements in the page can modify the contents of the shadow DOM by using property. In the mode, the shadow DOM is not accessible from outside using the property as it is in this case. open closed shadowRoot closed shadowRoot null Try doing this on a closed shadow DOM custom element: console.log(document.querySelector("demo-webc").shadowRoot) It returns . null and are extremely useful when building complex custom elements or web components. Diving deep into them is out of the scope of this article, but here are some good resources for them: Templates slots Using templates and slots - Web APIs | MDN Shadow DOM slots, composition Working with Slots and Web Components Styling Shadow DOM The shadow DOM can be styled either by: Constructing a object, inserting CSS in it using and attaching it to the shadow DOM using the property. CSSStyleSheet replaceSync() adoptedStyleSheets const shadowDOM = this.attachShadow({mode: "open"}) const styleSheet = new CSSStyleSheet() styleSheet.replaceSync(p { color: blue; }) shadowDOM.adoptedStyleSheets = [styleSheet] Declaring styles using a . <template> <template id="custom"> <head> <style>p { color: blue; }</style> </head> <p>Web Component</p> </template> const shadowDOM = this.attachShadow({ mode: "open" }) const template = document.querySelector("#custom") shadowDOM.appendChild(template.content.cloneNode(true)) Simply creating a tag and inserting CSS as text in it. style const style = document.createElement("style") style.textContent = p { color: blue; } shadow.appendChild(style) Creating Your Own Web Component # The first web component shown below is a custom button element, which opens a element. And the second web component involves a shadow DOM to pretty print JSON strings in HTML. dialog https://codepen.io/seekertruth/pen/LYaYqmo?embedable=true Similarly, you can create your own custom elements and use them anywhere you want. Also published . here