Orbiting Success: Building a Carousel with Modern Web Standards
- carousel
- css
- html
- javascript
- scroll snap
- typescript
The Carousel Quest
Carousels: the unsung heroes of web design. They effortlessly showcase images, highlight features, or tell a story in a swipe. But how do we craft one that’s smooth, responsive, and accessible? Let’s embark on a mission to build a carousel using the sleek capabilities of CSS scroll-snap
, and then give it that extra boost with some JavaScript ingenuity.
Laying the Foundations with HTML
First, we construct the skeleton of our carousel. By leveraging custom elements, we keep our HTML clean and avoid the clutter of unnecessary class names. Custom elements are the future—no JavaScript required!
Here’s our basic setup, featuring placeholder images from Lorem Picsum:
<carousel-container aria-label="Image carousel">
<carousel-content>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=1"
/>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=2"
/>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=3"
/>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=4"
/>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=5"
/>
</carousel-content>
</carousel-container>
Styling with CSS: The Art of Scroll-Snap
Now, let’s paint our carousel with CSS. The magic lies in scroll-snap
, ensuring each swipe lands perfectly on an image.
carousel-container {
display: flex;
align-items: center;
gap: 1rem;
}
carousel-content {
display: flex;
gap: 1rem;
padding-block: 1rem;
overflow-x: scroll;
/* the children of the carousel */
& > * {
border-radius: 25%;
width: 200px;
height: 200px;
}
}
But wait, let’s elevate it with scroll-snap
to make each image elegantly align when scrolled.
carousel-content {
display: flex;
gap: 1rem;
padding-block: 1rem;
overflow-x: scroll;
scroll-snap-type: x;
/* the children of the carousel */
& > * {
border-radius: 25%;
width: 200px;
height: 200px;
scroll-snap-align: center;
}
}
Enhancing the Experience with JavaScript
A carousel feels incomplete without navigation buttons. Let’s enhance our setup by introducing carousel-trigger
elements that control the flow.
Crafting the Triggers
<carousel-trigger direction="backward"></carousel-trigger>
The Backbone: CarouselContent and CarouselTrigger Classes
class CarouselContent extends HTMLElement {
shift(direction: "forward" | "backward") {
// total width of the scroll area / how many items there are
const scrollLength = this.scrollWidth / this.childElementCount;
// scroll forward or backward depending on the direction
this.scrollBy({
left: direction === "forward" ? scrollLength : -scrollLength,
});
}
}
customElements.define("carousel-content", CarouselContent);
class CarouselTrigger extends HTMLElement {
get direction() {
return this.getAttribute("direction") as "forward" | "backward";
}
/** returns the `carousel-content` element within the same parent */
get content() {
return this.parentElement?.querySelector(
"carousel-content",
) as CarouselContent;
}
connectedCallback() {
this.role = "button";
this.ariaHidden = "true";
this.classList.add("button", "button-ghost", "button-icon");
// https://lucide.dev/icons/
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`;
if (this.direction === "backward") this.style.rotate = "180deg";
this.addEventListener("click", () => this.content.shift(this.direction));
}
}
customElements.define("carousel-content", CarouselContent);
customElements.define("carousel-trigger", CarouselTrigger);
Graceful Degradation
What if JavaScript decides to take a break? We ensure our triggers stay hidden, maintaining a clean interface.
carousel-trigger:not(:defined) {
display: none;
}
To add some smoothness to our scroll, we sprinkle in scroll-behavior: smooth
:
carousel-content {
display: flex;
gap: 1rem;
padding-block: 1rem;
overflow-x: scroll;
scroll-behavior: smooth;
scroll-snap-type: x;
& > * {
border-radius: 25%;
width: 200px;
height: 200px;
scroll-snap-align: center;
}
}
The Grand Finale
Bringing it all together, here’s the complete masterpiece:
<script type="module" src="client.ts"></script>
<style>
carousel-container {
display: flex;
align-items: center;
gap: 1rem;
}
carousel-content {
display: flex;
gap: 1rem;
padding-block: 1rem;
overflow-x: scroll;
scroll-behavior: smooth;
scroll-snap-type: x;
& > * {
border-radius: 25%;
width: 200px;
height: 200px;
scroll-snap-align: center;
}
}
carousel-trigger:not(:defined) {
display: none;
}
</style>
<carousel-container aria-label="Image carousel">
<carousel-trigger direction="backward"></carousel-trigger>
<carousel-content>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=1"
/>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=2"
/>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=3"
/>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=4"
/>
<img
alt="A random image from picsum.photos"
src="https://picsum.photos/200?random=5"
/>
</carousel-content>
<carousel-trigger direction="forward"></carousel-trigger>
</carousel-container>
class CarouselContent extends HTMLElement {
shift(direction: "forward" | "backward") {
const scrollLength = this.scrollWidth / this.childElementCount;
this.scrollBy({
left: direction === "forward" ? scrollLength : -scrollLength,
});
}
}
class CarouselTrigger extends HTMLElement {
get direction() {
return this.getAttribute("direction") as "forward" | "backward";
}
get content() {
return this.parentElement?.querySelector(
"carousel-content",
) as CarouselContent;
}
connectedCallback() {
this.role = "button";
this.ariaHidden = "true";
this.classList.add("button", "button-ghost", "button-icon");
this.innerHTML = /* html */ `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg>
`;
// rotate if backward
if (this.direction === "backward") this.style.rotate = "180deg";
this.addEventListener("click", () => this.content.shift(this.direction));
}
}
customElements.define("carousel-content", CarouselContent);
customElements.define("carousel-trigger", CarouselTrigger);
Final Thoughts
Building a carousel isn’t just about sliding images—it’s about crafting an experience. By harnessing the power of CSS scroll-snap
and enhancing it with custom web components, we’ve created a carousel that’s not only visually appealing but also user-friendly and accessible. Embrace these modern web standards, sprinkle in some JavaScript magic, and watch your web projects orbit towards success!