DEV Community

Cover image for Found Simplicity & Elegance – Building a Simple Carousel with JavaScript
Tom J.
Tom J.

Posted on

1

Found Simplicity & Elegance – Building a Simple Carousel with JavaScript

Hey everyone!

I recently worked on a simple carousel for a client project. I'm absolutely enjoying this project—I didn't need to work on it myself, but I chose to dive in anyway because it's just so thrilling. I finally managed to find the best way to manage a project for its speedy and quality growth.

Alongside that, I also finally discovered the value in simplicity that accomplishes the goal perfectly without projecting a dev's (aka past me) need to prove themselves.

I've uploaded the code to GitHub Gist, and in this article, I'll walk you through how it works. You can also watch the uploaded live stream on YouTube.

The Goal

Create a responsive, smooth-scrolling carousel that:

  • Allows horizontal scrolling through items.
  • Has left and right arrow controls.
  • Hides the arrows appropriately when scrolling reaches the start or end.
  • Is SIMPLE, LIGHTWEIGHT, and EASY to integrate.

Embracing Simplicity

In the world of development in general, it's easy to get caught up in complex solutions, over-engineering features to showcase our skills. But usually, the simplest solutions are the way to go. This carousel is a testament to that philosophy.

I could download a library or use margins, or sometimes I even saw absolute positioning—but I didn't; all those solutions are too heavy without providing any meaningful gain.

I focused on using vanilla JavaScript and CSS to achieve the desired functionality. The result is a lightweight carousel that's easy to understand, maintain, and integrate into any project (yeah, I actually put it in place—or rather, all 6 places with specific design from Figma—in 30 minutes).

How It Works

view raw Carousel.js hosted with ❤ by GitHub
class GraceEvent {
El;
Event;
Callback;
ListenerOptions;
Paused = true;
constructor(el, event, callback, listener_options) {
this.El = el;
this.Event = event;
this.Callback = callback;
this.ListenerOptions = listener_options;
}
Init() {
this.StartListening();
}
StartListening() {
this.El.addEventListener(this.Event, this.Callback, this.ListenerOptions);
}
StopListening() {
this.El.removeEventListener(this.Event, this.Callback);
}
Resume() {
this.Paused = false;
this.StartListening();
}
Pause() {
this.Paused = true;
this.StopListening();
}
}
view raw GraceEvent.js hosted with ❤ by GitHub
class GraceWeb {
/** @type HTMLElement */
El;
Events = [];
/**
* @param el {HTMLElement}
*/
constructor(el) {
this.El = el;
}
Init() {
this.SetEvents();
}
Destroy() {
this.Events.forEach(event => event.StopListening());
this.Events = [];
}
SetEvents() {
}
NewData(data, all_data) {
}
SetSearchData(data) {
}
ListenEl(event_name, callback, listener_options) {
return this.Listen(this.El, event_name, callback, listener_options);
}
Listen(el, event_name, callback, listener_options) {
let event = new GraceEvent(el, event_name, callback, listener_options);
event.Init();
this.Events.push(event);
return event;
}
}
view raw GraceWeb.js hosted with ❤ by GitHub
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="GraceEvent.js" defer></script>
<script type="text/javascript" src="GraceWeb.js" defer></script>
<script type="text/javascript" src="Carousel.js" defer></script>
<style>
body {margin: 0; padding: 20px 50px; background-color: #1d1d1d; color: white; font-family: sans-serif; line-height: 150%;}
.Carousel {position: relative;}
.Carousel .In {display: flex; gap: 20px; overflow: auto; scrollbar-width: none; scroll-snap-type: x mandatory; scroll-behavior: smooth;}
.Carousel .In::-webkit-scrollbar {display: none;}
.Item {min-width: 300px; max-width: 350px; flex: 1; border-radius: 12px; border: 1px solid #aaaaaa; padding: 20px; scroll-snap-align: start;}
.Item * {margin-top: 0; margin-bottom: 0;}
.Item h3 {margin-bottom: 20px;}
.Arrow {position: absolute; top: calc(50% - 25px); width: 50px; height: 50px; border-radius: 100%; background: #ffffff; border: 1px solid #1d1d1d; cursor: pointer; transition: 250ms opacity; opacity: 1;}
.ArrowLeft {left: -25px;}
.ArrowRight {right: -25px;}
.Carousel.ScrollLeft0 .ArrowLeft {opacity: 0; pointer-events: none;}
.Carousel.ScrollRight0 .ArrowRight {opacity: 0; pointer-events: none;}
</style>
</head>
<body>
<div class="Carousel" data-type="carousel">
<div class="In" data-type="carousel-scroll">
<div class="Item" data-type="carousel-item">
<h3>1. The Wreck of Salvation</h3>
<p data-rows="3">The vastness of space was a silent graveyard, punctuated by the dim glow of dead stars. Amid this void floated the Salvation, a derelict colony ship long forgotten by those who launched it. Once a beacon of humanity’s hope, it now stood as a tomb for its crew and passengers. Its hull bore the scars of micro-meteor impacts and centuries of decay.<br>Inside, the emergency lights flickered intermittently, casting eerie shadows down the empty corridors. What remained of the ship's AI barely sustained itself, looping old broadcasts from Earth. Faint echoes of laughter and music from a world long gone reverberated through the metallic skeleton, a cruel reminder of what had been lost.</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>2. The Ghost Signal</h3>
<p data-rows="3">It was the ghost signal that first drew him in. Rion Thane, a scavenger eking out a living in the outer belts, intercepted it on a battered receiver. The transmission was garbled, broken by static, but unmistakably human. Against his better judgment, Rion charted a course toward the coordinates.<br>As he approached, the Salvation loomed like a giant carcass against the backdrop of the cosmos. Docking his small craft to the derelict, he felt the weight of history and something darker pressing against his chest. The ghost signal cut off abruptly as soon as his boots touched the ship’s floor, replaced by an oppressive silence.</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>3. Echoes of the Forgotten</h3>
<p data-rows="3">The interior of the Salvation was a labyrinth of broken corridors and frost-coated chambers. Personal belongings lay scattered—a child's toy, a diary with pages stuck together, a wedding ring. Rion resisted the urge to explore further; his goal was the bridge, where the black box might offer answers.<br>The air was stale, a mix of metal and decay. Strange markings, like claw marks, marred the walls. As Rion moved deeper, the ship seemed to breathe, the groans of its aged structure mimicking a heartbeat. And always, there was the faint sound of whispers, just out of earshot.</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>4. The Shadows Walk</h3>
<p data-rows="3">Rion reached the cryo chamber, where rows of frost-covered pods lined the walls. Many were shattered, their occupants reduced to bone and dust. A few pods still functioned, the occupants frozen in unnatural poses, their faces twisted in terror.<br>It was then he saw it—a shadow moving across the glass. Whirling around, Rion saw nothing but the faint outline of his own reflection. Yet he felt it, a presence watching him, something alive within the cold expanse of the ship. He quickened his pace, his breath fogging in the frigid air.</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>5. The Captain’s Last Log</h3>
<p data-rows="3">The bridge was a shattered command center, screens cracked and chairs overturned. Among the wreckage, Rion found the captain's log embedded in the console, miraculously intact. He plugged it into his handheld device, watching as fragmented recordings came to life.<br>“We encountered… something,” the captain’s voice trembled. “Not human. It came from the void. It got into the systems first. Then the crew.” The recordings grew erratic, the captain’s voice dissolving into screams. “We locked it in the lower decks. God help anyone who sets it free.”</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>6. The Thing Below</h3>
<p data-rows="3">Rion’s curiosity burned brighter than his fear. The mention of the lower decks and the possibility of alien life was too enticing. With trembling hands, he overrode the locks on the access shaft and descended into the depths of the Salvation.<br>The lower decks were a hellscape. Organic growths pulsed on the walls, oozing a viscous black fluid. The air was suffused with a foul stench that clung to Rion’s lungs. And then he heard it—a low growl, primal and hungry. He froze as a pair of glowing eyes pierced the darkness.</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>7. The Predator Awakes</h3>
<p data-rows="3">The creature emerged, a grotesque fusion of organic and mechanical parts. Its body shimmered with liquid metal, and tendrils writhed where limbs should have been. Rion raised his weapon, but the thing moved faster than thought, pinning him against the wall with a single motion.<br>It didn’t kill him. Instead, it studied him, its glowing eyes boring into his soul. Images flashed through Rion’s mind—stars collapsing, civilizations erased, and the endless hunger of the void. The creature spoke without words, its voice a cacophony of despair.</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>8. Assimilation</h3>
<p data-rows="3">Rion realized too late that he was being invaded. The creature's tendrils sank into his flesh, spreading cold fire through his veins. His screams echoed through the empty halls, blending with the wails of those who had come before him.<br>The Salvation was alive, its systems fused with the alien entity. Rion’s thoughts blurred, his memories consumed and replaced by the collective consciousness of the ship’s victims. He was no longer himself but a part of something vast and unknowable.</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>9. The New Signal</h3>
<p data-rows="3">Days, or perhaps weeks later, a new signal radiated from the Salvation. It was cleaner than the ghost signal Rion had intercepted but equally haunting. It spoke of riches beyond imagination, urging scavengers and explorers to come.<br>The ship waited patiently, its trap reset. The entity, now more powerful with Rion’s addition, coiled in the depths, ready to consume its next victim. The Salvation was no longer a tomb; it was a predator, luring the greedy and desperate with promises of salvation.</p>
</div>
<div class="Item" data-type="carousel-item">
<h3>10. The Endless Void</h3>
<p data-rows="3">Far from the Salvation, the signal reached another scavenger, another lonely soul in the void. They plotted their course toward the derelict, their mind filled with visions of wealth and discovery.<br>The cycle would continue. The entity would grow. The void would claim more lives. And in the vast emptiness of space, no one would hear them scream.</p>
</div>
</div>
<div class="Arrow ArrowLeft" data-type="arrow-left"></div>
<div class="Arrow ArrowRight" data-type="arrow-right"></div>
</div>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

The carousel's HTML is dead simple. I'll just mention one thing—classes are for UI devs, data- attributes are for JS devs—this separation is awesome for team development.

CSS takes care of all the animations for performance and ease of use and later changes.

JS is the real worker here; it performs super simple logic to achieve a fantastic (not just a “good enough”) job in making the simple move and calculation. It’s simple code as it is, and it even has performance optimization included.

Why This Approach Works

  • Performance: Less code and no external dependencies mean faster load times and better performance.
  • Maintainability: Simplicity makes it easier to understand and modify the code in the future.
  • Customization: It's easy to tweak and style according to different projects' needs.

Watch the Live Stream

If you're interested in seeing how I built this carousel step by step, check out the live stream on YouTube:

Watch the Live Stream

Final Thoughts – Let's Keep Learning

First, I used some 250kb carousel jQuery library. It had so many different transition effects and implementations, and it was awesome!... It wasn't... Every time a client asked for something, this thing just couldn't do it.

Next, I created our own using the jQuery framework. It had 500 lines plus CSS, and it was universally used for any project and design. It was awesome!... It wasn't... Any kind of bending for any project took literally 2 hours, and any project needed to be bent to it.

Then, I made a carousel for each project separately based on the same logic—margins and some effects on top and changing behavior on several responsive steps. It also wasn't awesome.

Finally—this thing now takes me 10 minutes and is perfectly suited for a given project and perfectly performant.

Of course, this requires collaboration with the graphics designer—if they want it simple, I can provide simple. But also, every other solution was unnecessarily bloated and large and just horrible, and even in the craziest carousels anyone can think of, the solution and code would be quite simple.

Do you have your own experiences seeing yourself change over the years?

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more