Skip to main content Accessibility Feedback

Let's create a Web Component from scratch!

Because I love Web Components so much, today, I thought we’d build an HTML Web Component from scratch. Let’s build a component that shows and hides text when a button is toggled.

Let’s dig in!

If you’d like help building Web Components for your site or converting existing DOM scripts over, get in touch. I’d be happy to help!

The HTML

The heart of an HTML Web Component is the HTML.

With this approach, the idea is to start with a baseline of fully functional HTML that you enhance into something more interactive once the component instantiates.

To that end, let’s add a button that will show or hide our content, and a div with our content in it. Since we don’t want the button to display until the Web Component is ready, we’ll add the [hidden] attribute to hide it.

<button hidden>Show Content</button>

<div>
	<p>Now you see me, now you don't!</p>
</div>

Now, we’ve got some content that’s visible by default.

Let’s wrap it in our custom element, which we’ll call show-hide. We’ll also add a [trigger] attribute to the button, and a [content] attribute to the content.

Our Web Component can use these once it instantiates to identify the toggle and content, respectively. This lets us put the button before or after the content, and gives us a bit of flexibility.

<show-hide>
	<button trigger hidden>Show Content</button>

	<div content>
		<p>Now you see me, now you don't!</p>
	</div>
</show-hide>

Defining the Web Component

Now that we’ve got our baseline of HTML, we can define our Web Component using the customElements.define() method.

We’ll define our show-hide custom element, and extend the HTMLElement class. In our constructor(), we’ll use the super() method to gain access to the parent class properties.

customElements.define('show-hide', class extends HTMLElement {

	/**
	 * Instantiate the Web Component
	 */
	constructor () {

		// Get parent class properties
		super();

	}

});

Defining properties

Next, let’s define some custom properties.

We’ll use the Element.querySelector() method to look for the [trigger] and [content] elements inside the custom element (this), and assign them to to the trigger and content properties, respectively.

If either element doesn’t exist, we’ll return to end our setup early.

/**
 * Instantiate the Web Component
 */
constructor () {

	// Get parent class properties
	super();

	// Get the elements
	this.trigger = this.querySelector('[trigger]');
	this.content = this.querySelector('[content]');
	if (!this.trigger || !this.content) return;

}

Setting up the DOM

With our properties defined, we can now enhance the DOM and setup our event listener.

First, let’s use the Element.removeAttribute() method to remove the [hidden] attribute from the button element, this.trigger.

We’ll also use the Element.setAttribute() method to add an [aria-expanded] attribute with a value of false. This tells screen readers that the button toggles content visibility, and what the current state of that content is.

/**
 * Instantiate the Web Component
 */
constructor () {

	// Get parent class properties
	super();

	// Get the elements
	this.trigger = this.querySelector('[trigger]');
	this.content = this.querySelector('[content]');
	if (!this.trigger || !this.content) return;

	// Setup default UI
	this.trigger.removeAttribute('hidden');
	this.trigger.setAttribute('aria-expanded', false);

}

Next, we’ll add the [hidden] attribute to the this.content element to hide it.

Then, we’ll add a click event listener to the this.trigger element. For simplicity, we’ll use the handleEvent() method native to Web Components to handle our event (more on that in a second), and can pass this in as our callback.

/**
 * Instantiate the Web Component
 */
constructor () {

	// ...

	// Setup default UI
	this.trigger.removeAttribute('hidden');
	this.trigger.setAttribute('aria-expanded', false);
	this.content.setAttribute('hidden', '');

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

}

Handling events

The handleEvent() method is part of the EventListener API, and has been around for decades.

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.

customElements.define('show-hide', class extends HTMLElement {

	/**
	 * Instantiate the Web Component
	 */
	constructor () {
		// ...
	}

	/**
	 * Handle events in the Web Component
	 * @param {Event} event The Event object
	 */
	handleEvent (event) {
		// Handle the event...
	}

});

Inside our handleEvent() method, we’ll first run the event.preventDefault() method to ensure the button doesn’t trigger any unexpected side-effects, like submitting a form.

/**
 * Handle events in the Web Component
 * @param {Event} event The Event object
 */
handleEvent (event) {
	
	// Don't let the button trigger other actions
	event.preventDefault();

}

Then, we’ll use the Element.getAttribute() method to get the value of the [aria-expanded] attribute on the this.trigger element.

If it has a value of true, the content is currently expanded and we should hide it. If not, it’s hidden and we should show it.

We’ll set or remove the [hidden] attribute on this.content accordingly, and update the value of the [aria-expanded] attribute to match the current state.

/**
 * Handle events in the Web Component
 * @param {Event} event The Event object
 */
handleEvent (event) {
	
	// Don't let the button trigger other actions
	event.preventDefault();

	// If the content is expanded, hide it
	// Otherwise, show it
	if (this.trigger.getAttribute('aria-expanded') === 'true') {
		this.trigger.setAttribute('aria-expanded', false);
		this.content.setAttribute('hidden', '');
	} else {
		this.trigger.setAttribute('aria-expanded', true);
		this.content.removeAttribute('hidden');
	}

}

Now, the content will show or hide when the button is toggled.

Here’s a demo on CodePen.

Styling

The nice thing about using appropriate ARIA attributes (like [aria-expanded]) for interactive elements is that you can also use them to style things based on the current state of the element.

For example, you could use the [aria-expanded] attribute to show different icons on the button based on whether or not the content is visible.

show-hide [aria-expanded="true"] {
	/* Styles for visible content */
}

show-hide [aria-expanded="false"] {
	/* Styles for hidden content */
}

Building HTML Web Components

I’ve helped organizations like NASA use Web Components to create interactive UI that’s simpler to implement and maintain.

If you’d like help on your project, get in touch and setup a call. I’d love to hear more about your project!

And if you’d like to learn how to do this yourself, I have a bunch of tutorials and demos over on the Lean Web Club.