luis-alberto-nKUlPRDqaBE-unsplash.jpg
Cover image for ron

Ron Northcutt Verified userVerified user

Head of DevRel

Appsmith

Ditch the Bloat : Building a Swipeable Carousel with Only CSS

Sometimes you can waste a ton of time trying to find a solution when it's actually easier and faster to create your own. This is such a story.

For our portal, we've wanted to update the homepage to group new content by type, making it easier to find. However, we also wanted each group to list multiple items offscreen so we could scroll horizontally to find them.That makes the homepage much more concise and easier to manage.

This should be simple, right? I mean, it's just a carousel. We are using Bootstrap 5, which includes a carousel component, so we started with that. It worked, but it only shows a single item. It doesn't really work well to show multiple items. We also looked at a bunch of JS libraries, but they added too much weight and destroyed our performance scores.

In the end, we decided to build a lightweight custom solution. Our requirements:

  • Be able to show 3 or 4 cards on the screen, but have multiple off-screen cards
  • Be able to scroll with a mouse or gesture AND use arrow keys to click through
  • Use as little JS as possible
  • Keep the CSS fairly lightweight
  • Be responsive!

Since we are using Drupal, we don't need to worry about formatting or rendering the cards, or even selecting which ones. We will create a module for that. But first, we need to just come up with a simple carousel that is as lightweight as possible. This is where the power of CSS comes into play. By using some more advanced CSS for scrolling actions, we can build a simple carousel that doesn't need any Javascript at all!

Want to see it in action? Checkout this CodePen.

Why Has This Been Hard?

To implement an image carousel up until just a couple of years ago, you needed JavaScript. Usually a lot of Javascript. An implementation would include absolute positioning items inside a container using CSS. Then calculating the carousel wrapper size and item sizes in real-time using JavaScript. And then add interactions by either updating the left or right values or updating the transform value using JavaScript again. This leaves you with a very basic implementation of the carousel. If you wanted any form of swipe interactions on a phone, you'd probably be better off reaching out to an external library.

So, this seemingly simple request is actually pretty hard to do... or, it was. Now, we have other options available.

Enter Scroll Snap

This is the secret sauce that makes this incredibly powerful and simple. scroll-snap lets you control the panning and scrolling behavior by defining snap positions. Content can be snapped into position as the user scrolls overflowing content within a scroll container, providing paging and scroll positioning.

Effectively, you have a horizontal scrolling element, but instead of just random scrolling, you can "snap" to specific scroll points. This is the basic need for a carousel with clickable scrolling - you just have it advance based on the size of the items in the list!

Let's go through the basics of how this works.

A Rough Example

Start by creating an HTML list with items that will function as your carousel wrapper and carousel items:

<ul class="list">
  <li class="item"><div class="content">Item 1</div></li>
  <li class="item"><div class="content">Item 2</div></li>
  <li class="item"><div class="content">Item 3</div></li>
  <li class="item"><div class="content">Item 4</div></li>
  <li class="item"><div class="content">Item 5</div></li>
  <li class="item"><div class="content">Item 6</div></li>
  <li class="item"><div class="content">Item 7</div></li>
  <li class="item"><div class="content">Item 8</div></li>
</ul>

Apply scroll-snap-type to the wrapper with a value of mandatory for the axis you want the scroll snapping to happen. Apply scroll-snap-align to the items with a value of start to make sure the browser will align the active item to the beginning of your wrapper.

After including some styles to add spacing between and around the carousel and its items, it will result in something like this:

.list {
  display: flex;
  gap: 2rem;
  padding: 2rem;
  list-style: none;
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
}

.item {
  flex-shrink: 0;
  width: 100%;
  height: 66vh;
  background-color: #ccc;
  scroll-snap-align: start;
}

.content {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  font-family: sans-serif;
  font-size: 3rem;
  font-weight: bold;
}

It starts with item 1 and scrolls smoothly to items and snaps to the start of the items. This way, no matter how you scroll, you will always end up aligning on one of the items in the list, so it is never cut off. The coolest part is that this works using swipe gestures, too. Make sure to test this in a browser on your mobile phone to feel how natural it feels!

Super Simple, But Not Quite There

At this point, we have a working carousel, and it doesn't use any JS! Total win. However, we still need to show multiple items and have buttons... so a bit of JS will be necessary. I know, I know... I said no JS. And we have achieved that! But, we do want to have buttons, and that will require JS. However, we can keep it incredibly light.

Hide the Scrollbar

Even though we are using the scrollbar to power the slide, we don't want or need to see it once we have buttons. So, let's just get it out of the way for now:

.list {
  /* ... */
  scrollbar-width: none;
}

Add Buttons

For a better user experience, you can add arrow buttons to your carousel. They will help by indicating your user that there is more content to see. This is where we have to cheat a bit, because we'll need a tiny bit of JavaScript.

First, add the buttons to the HTML and style them. I chose to keep it extremely simple for the purposes of this blog post. Technically, you should use button elements instead of links, but I want to reuse this in other ways, and buttons often have strong styling to overcome. Keeping this a link means it will be visually more stable.

<div class="list-wrapper">
  <ul class="list">
    <li class="item"><div class="content">Item 1</div></li>
    <li class="item"><div class="content">Item 2</div></li>
    <li class="item"><div class="content">Item 3</div></li>
    <li class="item"><div class="content">Item 4</div></li>
    <li class="item"><div class="content">Item 5</div></li>
    <li class="item"><div class="content">Item 6</div></li>
    <li class="item"><div class="content">Item 7</div></li>
    <li class="item"><div class="content">Item 8</div></li>
  </ul>
  <a class="navbutton previous" type="button">></a>
  <a class="navbutton next" type="button"><</a>
</div>
.list-wrapper {
  position: relative;
}

.navbutton {
  position: absolute;
  top: 50%;
  width: 3rem;
  height: 3rem;
  background: #ccc;
  font-weight: bold;
  color: #fff;
  font-size: 1rem;
  display: flex;
  align-items: center;
  justify-content: center;
}

.navbutton.previous {
  left: 1.5rem;
}

.navbutton.next {
  right: 1.5rem;
}

Now we have two simple arrow buttons floating on our carousel's left and right side. The final thing we need is a tiny bit of JavaScript to make the buttons work!

Adding JavaScript

Now, all we need to do is define very basic JS that advances the "carousel" by the width of the items. That way, the scroll will snap to the front of the closest item.

<script>
  let list = document.querySelector(".list");

  // We want to know the width of one of the items. We'll use this to decide how many pixels we want our carousel to scroll.
  let item = document.querySelector(".item");
  let itemWidth = item.offsetWidth;

  // For the previous button, we add a negative amount to go "backwards"
  document.querySelector(".navbutton.previous").addEventListener("click", function() {
    list.scrollBy({ left: -itemWidth, behavior: "smooth" });
  });

  // For the next button, we add a positive amount to go "forwards"
  document.querySelector(".navbutton.next").addEventListener("click", function() {
    list.scrollBy({ left: itemWidth, behavior: "smooth" });
  });
</script>

It Works!

The best thing about this is that it is super simple. Our performance wasn't impacted, and we don't have any extra dependencies, so this code is easy to extend and reuse. Of course, this is only an example. In order to make it ready for production, we need to take it to the next level, make it responsive, and more.

Next, we will look at building out two different versions of this technique. One is for use with the Appsmith platform, and one for this community portal!

  • Appsmith - how to create a custom widget using this approach.
  • Portal - how to create a Drupal module using this approach.