Make a basic web component
Created //
TL;DR // Learn how to make a basic carousel web component. Just want some working code? Skip to the Github repo or the examples page.
Let's make a web component
Web components are a way to create custom HTML elements. They're a way to encapsulate HTML, CSS, and JavaScript into a single, reusable package. The idea of reusable components is something that was popularized by frameworks like React and Vue. But web components are a native browser feature. That means you don't need a framework to use them.
I'm not going to get into why you should use web components. I'm just going to show you how to make one. There are plenty of articles out there that do a great job of explaining why they're important and where they came from. See the MDN Web Components page for more info.
We'll assume you have a basic understanding of HTML, CSS and javascript. And, that you're using a modern browser that supports web components.
In 2023, effectively every modern browser supports web components. See Can I Use for more info.
Index
What we're going to make
We're going to build a basic slide carousel web component like this:



Yeah, it's fairly basic. But that's ok! Our goal is to learn the fundamentals of web components, not to build 'the best carousel ever'.
The basics
A web component is made up of three parts:
- A custom HTML element (e.g.,
<slide-carousel>
) - A shadow DOM (e.g., the HTML hidden inside the custom element)
- A template (the reference HTML used to construct the custom element)
A web component is initialized with javascript. Yes, that means the client browser must have javascript enabled for your web component to work. There are definitely progressive enhancement concerns to address. That'll be another article for another time.
Your custom element name
Your custom element name can be almost anything you want. But there's one rule to follow: it must have a hyphen in the name.
In our example, we've decided to use <slide-carousel>
.
Let's get started
What should our carousel HTML look like? We want a carousel element, and every child element inside should be considered a slide.
The carousel should automatically create the navigation buttons and add functionality.
So, the developer should create markup something like this:
<slide-carousel>
<img src="i/img1.jpg">
<img src="i/img2.jpg">
<img src="i/img3.jpg">
</slide-carousel>
And the custom element should automatically generate something like this:
<slide-carousel>
<img src="i/img1.jpg">
<img hidden src="i/img2.jpg">
<img hidden src="i/img3.jpg">
#shadow-root
<button class="previous-slide">Previous</button>
<button class="next-slide">Next</button>
</slide-carousel>
Don't worry about the #shadow-root
thing yet. It means the <button>
elements are going to be created automatically and hidden inside the component.
Step 1: Create the basic javascript class
It only takes a very small amount of javascript to define a new custom element. Let's look at that, and a simple HTML carousel with a few images.
Javascript
class SlideCarousel extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const template = `<slot></slot>`;
this.shadowRoot.innerHTML = template;
}
}
CustomElements.define('slide-carousel', SlideCarousel);
HTML
<slide-carousel>
<img src="i/img1.jpg">
<img src="i/img2.jpg">
<img src="i/img3.jpg">
</slide-carousel>
Let's break this down.
- We create a new class
SlideCarousel
which will contain the code to create our web component - The constructor defines the HTML template
- The template
<slot></slot>
element brings in the child elements (the images) - We register the custom element with the browser, using
CustomElements.define()
What does that give us? Not much yet.
We'll see the images on the page, but there's nothing special about them. We haven't added any functionality yet.
Here's what it looks like at this point:



Step 2: Show the first slide, hide the others
We need to add code to manage the state of the carousel. That means showing one slide (the active one), and hiding the others. We'll add that when the component registers with the browser. When does that happen? It happens with the connectedCallback()
method.
This method fires when the component is added to the DOM. That means we can use it to set up the carousel.
class SlideCarousel extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const template = `<slot></slot>`;
this.shadowRoot.innerHTML = template;
// Keep track of how many slides, and which one is active.
this.totalSlides = 0;
this.activeSlide = 0;
}
connectedCallback() {
// Get all the slides.
const slides = Array.from(this.children);
if (slides.length < 1) return;
this.totalSlides = slides.length;
// Show the active slide, hide the others.
slides.forEach((slide, index) => {
if (index === this.activeSlide) {
slide.removeAttribute("hidden");
} else {
slide.setAttribute("hidden", "hidden");
}
});
}
}
customElements.define('slide-carousel', SlideCarousel);
Here's what it looks like now:



We've got a problem. Nothing's changed -- we're still showing all of the images. Why?
Because, when the connectedCallback()
method runs, the <slot>
element hasn't been populated yet. That means we haven't inserted any of the child <img>
elements yet. We need to wait until the <slot>
element has been populated. We have a timing problem.
Step 3: Fix the timing problem
We should listen for a slotchange
event. This event tells us when all of the <slot>
elements have been inserted.
Let's move what we were doing in connectedCallback()
into a new method called init()
. Then we'll call init()
from the slotchange
event.
class SlideCarousel extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const template = `<slot></slot>`;
this.shadowRoot.innerHTML = template;
const slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", event => {
this.init();
});
// Keep track of how many slides, and which one is active.
this.totalSlides = 0;
this.activeSlide = 0;
}
connectedCallback() {}
init() {
// Get all the slides.
const slides = Array.from(this.children);
if (slides.length < 1) return;
this.totalSlides = slides.length;
// Show the active slide, hide the others.
slides.forEach((slide, index) => {
if (index === this.activeSlide) {
slide.removeAttribute("hidden");
} else {
slide.setAttribute("hidden", "hidden");
}
});
}
}
customElements.define('slide-carousel', SlideCarousel);
Now we just see the one active slide, as expected:



Now we need to add navigation!
Step 4: Add navigation buttons
We'll add a Previous button and a Next button to cycle through the slides. The two buttons will be part of the Shadow DOM.
class SlideCarousel extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const template = `
<slot></slot>
<br>
<button id="prev">Previous</button>
<button id="next">Next</button>
`;
this.shadowRoot.innerHTML = template;
const slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", event => {
this.init();
});
// Keep track of how many slides, and which one is active.
this.totalSlides = 0;
this.activeSlide = 0;
}
connectedCallback() {
const previous = this.shadowRoot.getElementById("prev");
previous.addEventListener("click", event => {
this.activeSlide--;
this.updateSlides();
});
const next = this.shadowRoot.getElementById("next");
next.addEventListener("click", event => {
this.activeSlide++
this.updateSlides();
});
}
init() {
// Get all the slides.
const slides = Array.from(this.children);
if (slides.length < 1) return;
this.totalSlides = slides.length;
this.updateSlides();
}
updateSlides() {
if (this.activeSlide < 0) this.activeSlide = this.totalSlides - 1;
if (this.activeSlide > this.totalSlides - 1) this.activeSlide = 0;
// Show the active slide, hide the others.
Array.from(this.children).forEach((slide, index) => {
if (index === this.activeSlide) {
slide.removeAttribute("hidden");
} else {
slide.setAttribute("hidden", "hidden");
}
});
}
}
customElements.define('slide-carousel', SlideCarousel);
We now have buttons that let us cycle through the carousel slides. It's like a real carousel!



Step 5: Add styling
Let's keep going. What if we want to style those buttons we added?
We can try, with something like this:
slide-carousel button {
background-color: red;
}
But... it won't do anything. Why? Because the buttons are inside the Shadow DOM. We need to expose the buttons to the "outside world" so we can style them. We do this with a part
attribute.
Let's update the line that defines the HTML within the Shadow DOM.
Inside the constructor
we're going to change
const template = `
<slot></slot>
<br>
<button id="prev">Previous</button>
<button id="next">Next</button>
`;
...to this:
const template = `
<slot></slot>
<br>
<button part="button previous" id="prev">Previous</button>
<button part="button next" id="next">Next</button>
`;
Now we can style those elements like this:
slide-carousel::part(previous) {
background: yellow;
}
slide-carousel::part(next) {
background: green;
}



The full code
Here's the final code for our page with the custom carousel web component.
HTML
<!doctype html>
<html lang="en">
<head>
<script>
class SlideCarousel extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
const template = `
<slot></slot>
<br>
<button part="button previous" id="prev">Previous</button>
<button part="button next" id="next">Next</button>
`;
this.shadowRoot.innerHTML = template;
const slot = this.shadowRoot.querySelector("slot");
slot.addEventListener("slotchange", event => {
this.init();
});
// Keep track of how many slides, and which one is active.
this.totalSlides = 0;
this.activeSlide = 0;
}
connectedCallback() {
const previous = this.shadowRoot.getElementById("prev");
previous.addEventListener("click", event => {
this.activeSlide--;
this.updateSlides();
});
const next = this.shadowRoot.getElementById("next");
next.addEventListener("click", event => {
this.activeSlide++
this.updateSlides();
});
}
init() {
// Get all the slides.
const slides = Array.from(this.children);
if (slides.length < 1) return;
this.totalSlides = slides.length;
this.updateSlides();
}
updateSlides() {
if (this.activeSlide < 0) this.activeSlide = this.totalSlides - 1;
if (this.activeSlide > this.totalSlides - 1) this.activeSlide = 0;
// Show the active slide, hide the others.
Array.from(this.children).forEach((slide, index) => {
if (index === this.activeSlide) {
slide.removeAttribute("hidden");
} else {
slide.setAttribute("hidden", "hidden");
}
});
}
}
customElements.define('slide-carousel', SlideCarousel);
</script>
<style>
slide-carousel::part(button) {
/* Add styles for either of the buttons */
}
slide-carousel::part(previous) {
/* Add styles for the "previous" button */
}
slide-carousel::part(next) {
/* Add styles for the "next" button */
}
</style>
</head>
<body>
<h1>This is my custom carousel web component</h1>
<slide-carousel>
<img src="i/img1.jpg">
<img src="i/img2.jpg">
<img src="i/img3.jpg">
</slide-carousel>
</body>
</html>
We've put this all in a single HTML file. No, that's not the best way to do it. But it's the easiest way to get started and learn. When you're doing this "for real" put the javascript in its own file.
Are we done?
For the purposes of this article, yes! We've built a basic carousel web component. It's not perfect, but it's a good start.
There are definitely other things that could be explored. For example:
- How to provide component configuration via attributes
- Progressive enhancement (what happens if javascript is disabled?)
- Additional features (pagination, autoplay, etc.)
- Customization (button labels, HTML structure, etc.)
- Robustness (proper implementation of accessibility)
But the basic premise of how you make a web component has been covered.
If you want to see a more complete example, check out the Carousel examples page and the Github repo.
References
Let's be clear -- there's no sense in re-inventing the wheel and building it all yourself. This article was about demonstrating the basic premise behind native web components.
If you want a good set of solid components I'd highly recommend starting here:
// ka