Back to blog
Dec 17, 2024
8 min read

Part 1: Getting Started with a basic Web Component

Covering Web Components soup to nuts by starting with something basic, and building from there.

In my last blog post I covered a high level introduction into Web Components. In this post we’re going to actually lay the groundwork for creating a web component by introducing some tooling, and setting up the basics to get started. Then we’ll create a custom element and play with it a bit.

Setting up the development playground

As in real life, in this post series I’ll be using my web development IDE and some basic web development skills to create a “playground” for building, debugging and testing a Web Component. I adopted VSCode as my IDE of choice about 3 years ago so much of what I cover can be accomplished there, or with any IDE, a modern browser with developer tools available (I’ll be using Chrome and the developer tooling built into Chrome), and a running web server.

VSCode has some available extensions that will also be helpful as we go through this process, namely:

  • Live Server: Will launch a local running web server from within VS Code, great for testing on demand your work-in-progress code.
  • HTML Boilerplate: Creates boilerplate HTML markup, because who wants to code that by hand each time?
  • Lorem Ipsum: Generates Lorem Ipsum text as filler.

Creating our base project

To create the base project, create a workspace folder and create three files (we’ll walk through that in a moment):

  • index.html: A basic web page complete with HTML tag, head and body tags. If you’re using HTML Boilerplate, use the “html5” default and you’ll get everything you need to get started.
  • style.css: Global stylesheet for your web page. Include a link tag in the head of index.html for the stylesheet
  • Create a new folder called my-component inside your workspace, and in that folder create a file called my-component.js. This will be where the code lives for your component. Include that file in a script tag within the body tag of index.html, just as you would include any javascript needed for a web page.

Scaffolding an HTML project to build your component

Open index.html in VSCode and begin typing “html”. You should see an autopopulated list of page templates to select from (these are provided by the HTML Boilerplate extension), select html:5 from the autocomplete list.

You should get some boilerplate HTML that looks somethinng like this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

Below the second <meta> tag and above the <title> tag, insert a blank line and type link:css and hit return. This will add a boilerplate link to your stylesheet file as follows:

<link rel="stylesheet" href="style.css">

In the <body> tag, just before the closing tag, type script:module and hit return, you’ll get a boilerplate script tag as follows:

<script type="module" src=""></script>

In the src attribute fill in the path to the my-component.js, with the script tag looking like this when you’re finished:

<script type="module" src="/my-component/my-component.js"></script>

Your scaffolding is complete.

Open /my-component/my-component.js in your IDE. Just to verify that your file is being loaded properly, add the following to the file and save it:

console.log('I am loaded!')

Now in the left sidebar of VSCode, right click on index.html and select “Open with Live Server” from the popup menu. When the browser launches, open the Developer tools and navigate to the Console, and verify that the output “I am loaded!” is appearing in the console window.

Congrats, you’re ready to go.

Creating a basic custom Web Component

Edit /my-component/my-component.js, remove the console.log line and replace it with the following:

class MyComponent extends HTMLElement {
  constructor() {
    super()
    console.log("my-component is loaded.")
  }
}

customElements.define('my-component', MyComponent)

Now open index.html and add the following in the <body> tag, above the <script> tag:

<my-component></my-component>

Save index.html, and check the Console in Developer Tools, you should see the text my-component is loaded..

You have written the minimum amount of code needed to create your first custom Web Component.

Now let’s break that down.

The Breakdown of the basic component

The custom Web Component, my-component consists of the following:

  • A class that extends HtmlElement, which defines the functionality of the component.

  • A call to the define() method of Window.customElements (which can be shortened to customElements, as Window is assumed if no parent object is specified), passing in the following arguments:

  • name: The name of the tag that defines the custom component, which must begin with a lowercase letter, contain a hyphen (-) in the name, and in general meet the criteria of a valid custom HTML element name.

  • The class that contains the functionality associated with the custom element. This can be a class name, or a call to the custom component’s constructor function (we are using the class name in our example).

The element’s class must, at a minimum, define a constructor. Inside that constructor is a call to the parent constructor using the super() method. This is needed so that your custom component inherits the functionality implicit in the HtmlElement parent class.

The remainder of the code in the constructor initializes the component. In our case, the only functionality is a console log message (not required, but included to test that the component is functioning when placed in an HTML page.)

Adding some Functionality — responding to attribute changes

Custom Web Components inherit the same basic behaviors as other HTML elements, including the ability to utilize attributes, and more specifically, to respond to changes in those attributes. To do this, a custom element’s defining class needs to have the following:

  • A static property called observedAttributes, whose value is an array containing the names of all the attributes for which the custom element needs to receive a change notification.
  • An implementation of the lifecycle callback method attributesChangedCallback(). This method is called whenever an attribute included in the list of names in the observedAttributes property is added, modified, removed or replaced.

Let’s add a name attribute to our Web Component, and log a message to the console whenever that attribute is added, modified or deleted.

Above the constructor() method, add the following:

static observedAttributes = ["name"]

And below the closing curly bracket of the constructor() method, add the following:

attributeChangedCallback(attribute, originalValue, newValue) {
  console.log(`Attribute ${attribute} has changed from ${originalValue} to ${newValue}.`)
}

and save the code.

Now open index.html and edit the <my-component> tag to add a name attribute with the value of John Doe, save the file, and then look at the output of the console in the Developer Tools. In addition to the message logged by the constructor, you should also see the following message:

Attribute name has changed from null to John Doe.

Now, my-component is starting to get a little more interesting.

In the javascript console, type in the following and hit return:

document.getElementsByTagName('my-component')[0].setAttribute('name', 'Judy Blume')

You should see the following message in the console, which was logged when a lifecycle event triggered the attributeChangedCallback() method:

Attribute name has changed from Jane Doe to Judy Blume.

Other Lifecycle Callbacks available to Custom Web Components

Custom Web component elements include three other lifecycle callbacks, as follows:

  • connectedCallback(): called each time the custom Web Component tag is added to a document.
  • disconnectedCallback(): called each time the custom Web Component tag is removed from a document.
  • adoptedCallback(): Called each time the tag is moved to a new document.

Let’s add these lifecycle event callbacks to my-component. Edit /my-component/my-component.js, and add the following below the closing curly bracket for the attributeChangedCallback() method, and save the file:

  connectedCallback() {
    console.log("my-component has been added to page.");
  }

  disconnectedCallback() {
    console.log("my-component has been removed from page.");
  }

  adoptedCallback() {
    console.log("my-component has moved to new page.");
  }

Now check the output in the console. You should see:

my-component is loaded
Attribute name has changed from null to Jane Doe.
my-component has been added to page.

So, in addition to initializing our component and setting the attribute value to that assigned in the attribute’s markup in index.html, we also triggered a lifecycle event that called connectedCallback() when the page markup was rendered, and the element was added to the page.

Now, type the following in the console:

document.getElementsByTagName('my-component')[0].remove()

This triggers the lifecycle event callback disconnectedCallback(), logging the following message in the console:

my-component has been removed from page.

Finally, type the following in the console:

var element = document.createElement('my-component')
element.setAttribute('name', 'Elvis Presley')
document.body.appendChild(element)
element.setAttribute('name', 'Jim Smith')

You should see the following output:

my-component is loaded
Attribute name has changed from null to Elvis Presley.
my-component has been added to page.
Attribute name has changed from Elvis Presley to Jim Smith.

Congratulations, you have a fully functional component complete with lifecycle event callbacks.

In my next post, we’ll add some far more interesting functionality to my-component by attaching and leveraging the component’s shadowDOM.