How to make simple slideshow using cool CSS and JS features
Tags: javascript, css, jsproxy, cssvariables
Published: 2022-11-13 00:46:00

Hello. Today I'm making a simple slideshow using cool CSS and JS features.
I'm gonna be using:

- CSS variables
- CSS transformations
- CSS functions 
- CSS grid
- JS Proxy

## The HTML

```html
<div class="slideshow">
  <div class="slides-container">
    <div class="slides">
      <img class="slide" src="https://picsum.photos/600/300?v=1" alt="">
      <img class="slide" src="https://picsum.photos/600/300?v=2" alt="">
      <img class="slide" src="https://picsum.photos/600/300?v=3" alt="">
      <img class="slide" src="https://picsum.photos/600/300?v=4" alt="">
      <img class="slide" src="https://picsum.photos/600/300?v=5" alt="">
    </div>
  </div>
  <div class="bullets">
  </div>
  <div class="buttons">
    <button type="button" class="prev">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
        <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
      </svg>

    </button>
    <button type="button" class="next">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
        <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
      </svg>
    </button>
  </div>
</div>
``` 
Let's break it down.

We have a div containing the whole slideshow with a class of `.slideshow`. This container has 3 immediate children. The first is the `.slides-container` which will act as the "window" behind which our slides will be showing. Then there is an empty `.bullets` container where we will dynamically put the bullets based on the number of slides. And the last one is the `.buttons` container, with the `.prev` and `.next` buttons.

Inside the `.slides-container` there is another container with a class of `.slides` which holds all our slides. This div will be moving right and left behind our "window" to reveal the active slide.

For the images I used pictures from <a href="https://picsum.photos" target="_blank">https://picsum.photos</a>, and for the `.prev` and `.next` buttons I used some icons from <a href="https://heroicons.com" target="_blank">https://heroicons.com</a>.

## The CSS

First, let's define some CSS variables that we will use later.

```css
:root {
  --max-size: 600px;  
  --size: min(var(--max-size), 100vw);
  --active: 0;
}
```
The `--max-size` is the maximum size of each slide. 
The `--size` is the calculated size of the whole slideshow. It will be 100vw for screens smaller than `--max-size`, and `--max-size` for larger screens.
The `--active` variable holds the index of the active slide.

Now, the outer container will be a CSS grid with 1 column and 2 rows. The first row will contain the slides and the buttons, and the second row will contain the bullets.

```css
.slideshow {
  display: grid;
  grid-template-rows: 1fr auto;
  grid-template-columns: 1fr;
  gap: 10px;
  width: var(--size);
}
```

Then, our "window". The `.slides-container` will sit in column 1 and row 1 of the parent grid. It will create a hole (a window), by having a transparent background and hiding anything overflowing.

```css
.slides-container {
  grid-column: 1;
  grid-row: 1;
  overflow: hidden;
}
```

Then the `.slides`. This is a div containing all our slides. It has  `display: flex` to put everything in a row, and it will move around to reveal the active slide. The interesting part here is the calculation of the container position based on the active slide and the size of the slideshow. 
It is `-1 * size * active`. Let's say the size is 600px. When the active slide is the 0th one, then it will give us 0 move on the x-axis. When the active is the 1st, then it will give `-1 * 600px * 1 = -600px` so it will move 600px to the left.

```css
.slides {
  display: flex;
  transform: translateX(calc(-1 * var(--size) * var(--active)));
  transition: transform 0.3s ease;
}
```

Our images need to have a width equal to the `--size` variable, in order to be responsive. This is because their parent is really long and we can't rely on the image taking up all the available width and having a height of auto.

```css
img {
  width: var(--size);
}
```

Then the buttons container. This div has to sit on top of the slides. I don't like using `position: absolute` so I used the grid feature of putting multiple elements in the same grid cell. So I put it in the same grid cell as the `.slides-container`. The problem with this is that I cannot click on the slides themselves. If the slides had a link I wouldn't be able to click on it because another div is in front of it. To tackle this problem I added `pointer-events: none` at the buttons container. Now I cannot click the buttons. So I add `pointer-events: all` at the buttons.
The buttons in this container will be in the middle vertically and at the sides horizontally. 

```css
.buttons {
  z-index: 1;
  display: flex;
  justify-content: space-between; /* Horizontally at the sides */
  align-items: center; /* Vertically in the middle */
  padding: 10px;
  grid-column: 1;
  grid-row: 1;
  pointer-events: none;
}
```

I don't think it is essential to include the buttons styling here. I have a codepen later to showcase the slideshow demo.

The `.bullets` container sits in row 2 of the slideshow grid. It aligns the bullets at the center.

```css
.bullets {
  grid-column: 1;
  grid-row: 2;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
}
```

Each bullet is round and the one corresponding to the active slide has different styling.

So far we have this. It doesn't work yet, but it's styled.

{% codepen https://codepen.io/istavros/pen/BaVRGGb %}

## The javascript

First, let's keep some useful stuff in variables.

```javascript
// We keep references to all needed elements.
const slideshow = document.querySelector(".slideshow");
const slidesContainer = slideshow.querySelector(".slides");
const slides = slideshow.querySelectorAll(".slide");
const bulletContainer = slideshow.querySelector(".bullets");
const prev = slideshow.querySelector(".prev");
const next = slideshow.querySelector(".next");
const length = slides.length;
```

Then, let's create the bullets based on the slide count. I use documentFragmet here to insert new elements into the DOM.

```javascript
// Automatically create the bullets based on
// the amount of slides we have.
const fragment = document.createDocumentFragment();
slides.forEach((slide, index) => {
  const bullet = document.createElement("span");
  bullet.classList.add("bullet");
  if (!index) {
    // Set the first bullet as active
    bullet.classList.add("active");
  }
  // Add the click functionality to the bullet
  bullet.addEventListener("click", () => active.slide = index);
  fragment.appendChild(bullet);
});
bulletContainer.appendChild(fragment);

// Keep a reference to the bullets
// so we can add/remove the .active class.
const bullets = bulletContainer.querySelectorAll(".bullet");
```

Now let's add functionality to the buttons. When clicking on the `.next` button we want to increase the `active` index by 1, and when clicking on the `.prev` button we want to decrease the `active` index by 1.
When we reach the last slide and click next, it should go back to the first. And when we are at the first and click previous it should go to the last one. However, I don't really like to put this logic into the click handlers, because by clicking `.next` I don't care which one is the next one. Just show me the next. So I used the magic of JavaScript Proxy. You may have noticed that in the bullet click listener I used `active.slide` to set the active slide index. Here is how it goes.

```javascript
// We use Proxy to include some kind of reactivity.
// By just changing active.slide, the DOM gets
// changed automatically, without the need to
// make the DOM changes within the event handlers.
const active = new Proxy(
  { slide: 0 },
  {
    set(obj, prop, value) {
      if (prop == "slide") {
        // If we click "prev" while on the first slide
        // then go to the last one.
        if (value < 0) {
          value = length - 1;
        }

        // If we click "next" while on the last slide
        // then go to the first one.
        if (value >= length) {
          value = 0;
        }

        // Make the appropriate DOM changes
        slidesContainer.style.setProperty("--slide", value);
        bullets[obj[prop]].classList.remove("active");
        bullets[value].classList.add("active");
      }

      // Actually set the active.slide prop to it's new value
      obj[prop] = value;
      return true;
    }
  }
);
```
As stated in the [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#description) website
> The Proxy object allows you to create an object that can be used in place of the original object, but which may redefine fundamental Object operations like getting, setting, and defining properties.

I give the Proxy a target object as the first argument, and a handler as the second argument. Into the handler there is a setter function.
The first argument `obj` is the target object `{ slide: 0 }`. The second argument `prop` is the property we are setting, in our case `slide`. And the third argument is the value we are setting to the property.
Into the setter is where I put all the logic for calculating which slide is the one that should be showing, and making the appropriate DOM changes. We can see that the only DOM change that has to be done is to set the `--active` CSS variable the the appropriate active slide index, and to toggle the `.active` class on the corresponding bullet.

With the help of the JavaScript Proxy, I am able to separate concerns and add very simple click handlers at the `.prev` and `.next` buttons.

```javascript
// Add the prev/next functionality to the buttons
prev.addEventListener("click", () => active.slide--);
next.addEventListener("click", () => active.slide++);
```
Here is the completed codepen.

{% codepen https://codepen.io/istavros/pen/JjZGBpY %}

And finally here is another codepen. It's the same as the last one, but if you press space, it goes 3D to show how the elements are placed on top of each other, and how the "window" works.

{% codepen https://codepen.io/istavros/pen/QWxvJwJ %}

I hope you liked the techniques I used to make this slideshow. I would love to hear better alternatives. I'm always up for learning something new.

Thanks for reading this far!