Skip to main content Accessibility Feedback

Your first Web Component

Yesterday, we talked about why Web Components won’t replace React or Vue. Today, let’s look at how to create your first Web Component.

Let’s dig in!

A really basic example

Let’s kick things off with the most generic and overused of examples: a counter button.

<button>Clicked 0 Times</button>

When the user clicks the button, we want to update it to to display the number of times it’s been clicked.

Defining our Web Component

To start, we’ll wrap our button in a custom element, wc-count.

<wc-count>
	<button>Clicked 0 Times</button>
</wc-count>

Next, we’ll use the customElements.define() method to define our custom element, and pass in a JavaScript class to handle the custom behavior.

With a Web Component, the class always extends the HTMLElement class. This lets it inherit all of the normal behaviors of an HTML element.

customElements.define('wc-count', class extends HTMLElement {
	// ...
});

Next, we’ll create a constructor() method to instantiate the class on our custom element.

Inside the constructor(), we need to run the super() method first. This allows the instance to automatically inherit all of the properties of the parent class, HTMLElement.

customElements.define('wc-count', class extends HTMLElement {

	/**
	 * The class constructor object
	 */
	constructor () {

		// Always call super first in constructor
		super();

		// Now we're in business!
		console.log(this);

	}

});

Once we load this into our page, our new class will automatically instantiate each <wc-count> element on the page as a unique instance.

Here’s a demo on CodePen.

Saving state data

We want to track the count, the number of times the button has been clicked.

Let’s add a this.button property that stores the button element. In our class, this is the wc-count custom element. We can use the Element.querySelector() method to find the button inside it.

We’ll also add a this.count property with a default value of 0.

/**
 * The class constructor object
 */
constructor () {

	// Always call super first in constructor
	super();

	// Instance properties
	this.button = this.querySelector('button');
	this.count = 0;

}

These are not reactive state like you’d see in a React component. Updating the value of this.count does not cause any sort of automatic UI updating.

This is just a convenient way to store data unique to each wc-count element.

Adding interactivity

When this.button is clicked, we want to update this.count, then update the text in this.button to reflect the new amount.

Let’s add a click event listener to this.button. We’re going to pass this in as the second argument instead of a callback method. I’ll explain why in a second.

/**
 * The class constructor object
 */
constructor () {

	// Always call super first in constructor
	super();

	// Instance properties
	this.button = this.querySelector('button');
	this.count = 0;

	// Listen for click events
	this.button.addEventListener('click', this);

}

If you listen for an event with the addEventListener() method, you can pass in an object instead of a callback function as the second argument.

As long as that object has a handleEvent() method, the event will be passed into it, but this will maintain it’s binding to the object.

In our class, we can add a handleEvent() method that will let us handle the click event, but still let us have easy access to the instance (this) so that we can access this.button and this.count.

customElements.define('wc-count', class extends HTMLElement {

	/**
	 * The class constructor object
	 */
	constructor () {
		// ...

		// Listen for click events
		this.button.addEventListener('click', this);

	}

	/**
	 * Handle events
	 * @param  {Event} event The event object
	 */
	handleEvent (event) {
		console.log('clicked!', this);
	}

});

Here’s another demo on CodePen.

Updating the state and UI

Now, we’re ready to update this.count and the text inside this.button.

Inside the handleEvent() method, we’ll increase this.count by 1 using the increment operator (++). Then, we’ll update the textContent inside this.button.

/**
 * Handle events
 * @param  {Event} event The event object
 */
handleEvent (event) {
	this.count++;
	this.button.textContent = `Clicked ${this.count} Times`;
}

Here’s a demo with the UI update.

You’ll notice that each button has its own state. Clicking one does not affect the count of the other.

Addressing accessibility

We should also announce the UI update to screen readers.

Inside the constructor(), let’s add an [aria-live] attribute with a value of polite to this.button.

/**
 * The class constructor object
 */
constructor () {
	// ...

	// Listen for click events
	this.button.addEventListener('click', this);

	// Announce UI updates
	this.button.setAttribute('aria-live', 'polite');

}

Here’s one last demo.

What’s next?

This week, we’ll look at how you can add options and customizations to Web Components to make them more flexible.

We’ll look at how you would author this using a traditional JavaScript library, so you can see the difference. In my opinion, Web Components make the authoring experience much, much easier (both for the creator and the person using it).

As always, if you have any questions, let me know!