<div class="content">

  <div class="pagination">
  </div>

  <div class="state">
    &nbsp;
  </div>

  <div class="controls">
    <button class="control control--prev" aria-label="Previous">
      <svg class="icon" viewBox="0 0 32 32">
        <path d="M20 28a1 1 0 0 1-.521-.146l-18-11a1.002 1.002 0 0 1 0-1.708l18-11A.999.999 0 0 1 21 5v6.11a1 1 0 0 1-.479.854L13.918 16l6.603 4.035c.297.182.479.506.479.854V27a1 1 0 0 1-1 1zM3.918 16 19 25.217V21.45l-7.521-4.596a1 1 0 0 1 0-1.707L19 10.55V6.783L3.918 16z" />
        <path d="M30 28a1 1 0 0 1-.521-.146l-18-11a1.002 1.002 0 0 1 0-1.708l18-11A.999.999 0 0 1 31 5v22a1 1 0 0 1-1 1zM13.918 16 29 25.217V6.783L13.918 16z" />
      </svg>
    </button>
    <button class="control control--play-pause" aria-label="Play/Pause">
      <svg class="icon icon--play" viewBox="0 0 32 32">
        <path d="M7 28a.999.999 0 0 1-1-1V5a1 1 0 0 1 1.521-.854l18 11a1.001 1.001 0 0 1 0 1.708l-18 11A1 1 0 0 1 7 28zM8 6.783v18.434L23.082 16 8 6.783z" />
      </svg>
      <svg class="icon icon--pause" viewBox="0 0 32 32">
        <path d="M13 28H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v22a1 1 0 0 1-1 1zm-5-2h4V6H8v20zM25 28h-6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v22a1 1 0 0 1-1 1zm-5-2h4V6h-4v20z" />
      </svg>
    </button>
    <button class="control control--next" aria-label="Next">
      <svg class="icon" viewBox="0 0 32 32">
        <path d="M12 28a1 1 0 0 1-1-1v-6.111c0-.348.182-.672.479-.854L18.082 16l-6.603-4.035A1.001 1.001 0 0 1 11 11.11V5a1 1 0 0 1 1.521-.854l18 11a1.002 1.002 0 0 1 0 1.708l-18 11A1 1 0 0 1 12 28zm1-6.55v3.767L28.082 16 13 6.783v3.767l7.521 4.596a1.001 1.001 0 0 1 0 1.708L13 21.45z" />
        <path d="M2 28a1 1 0 0 1-1-1V5a1 1 0 0 1 1.521-.854l18 11a1.002 1.002 0 0 1 0 1.708l-18 11A1 1 0 0 1 2 28zM3 6.783v18.434L18.082 16 3 6.783z" />
      </svg>
    </button>
  </div>

  <a target="_blank" href="https://muffinman.io/blog/css-animations-instead-of-js-timers/">Blog post</a>
</div>
// ----- GLOBAL
* {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

body {
  background: #98ede0;
  transition: background 500ms;
  font-family: ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa,
    Manjari, "Arial Rounded MT", "Arial Rounded MT Bold", Calibri,
    source-sans-pro, sans-serif;
}

.content {
  margin: 0 auto;
  max-width: 500px;
  height: 100vh;
  height: 100svh;
  min-height: 400px;
  display: flex;
  flex-direction: column;
  padding: 40px 40px 80px; // 80px to push it a bit and center visually
  justify-content: center;
}

a {
  transition: color 200ms;
  color: rgba(0, 0, 0, 0.5);
  border-radius: 5px;
  text-decoration: none;
  margin: 40px auto 0;

  &:hover {
    color: black;
    text-decoration: underline;
  }

  &:focus-visible {
    color: black;
    outline: 3px solid rgba(255, 255, 255, 0.3);
    outline-offset: 5px;
  }
}

// ----- PAGINATION
.pagination {
  display: flex;
  gap: 10px;
  height: 4px;
  min-height: 4px;
}

.pagination-item {
  border-radius: 100px;
  height: 100%;
  flex: auto;
  background: rgba(0, 0, 0, 0.08);
  overflow: hidden;
  border: 0;
  cursor: pointer;
}

@keyframes progress {
  from {
    width: 0;
  }
  to {
    width: 100%;
  }
}

.pagination-progress {
  flex: auto;
  background: #333;
  height: 100%;
  width: 0;

  .pagination-item--running & {
    animation: progress 3s linear forwards;
  }

  .pagination-item--done & {
    width: 100%;
  }

  .pagination--paused & {
    animation-play-state: paused;
  }

  // In Firefox we can use non-standard selecetor to pause when window is inactive:
  :-moz-window-inactive & {
    animation-play-state: paused;
  }
}

// ----- CONTROLS

.controls {
  display: flex;
  justify-content: center;
  gap: 12px;
}

.control {
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 100px;
  border: none;
  outline: none;
  height: 40px;
  flex: 40px 0 0;
  max-width: 100px;
  background: rgba(255, 255, 255, 0.3);
  transition: transform 200ms, background-color 200ms;
  will-change: transform, background-color;
  font-size: 20px;

  &:hover {
    cursor: pointer;
    transform: scale(1.15);
    background: rgba(255, 255, 255, 0.45);
  }

  &:focus {
    background: rgba(255, 255, 255, 0.6);
  }
}

.icon {
  width: 20px;
}

.icon--play {
  margin-right: -2px; // To visually center;
}

// Hacky selectors to hide play/pause icons
// I was lazy to refactor or introduce more javascript
.pagination--paused ~ .controls .icon--pause,
.pagination:not(.pagination--paused) ~ .controls .icon--play {
  display: none;
}

// ----- STATE
.state {
  font-size: 120px;
  text-align: center;
}
View Compiled
function getItem(index) {
  const item = document.createElement('button');
  item.classList.add('pagination-item');
  item.addEventListener('animationend', next);
  item.addEventListener('click', () => update(index));

  const progress = document.createElement('div');
  progress.classList.add('pagination-progress');

  item.appendChild(progress);

  return item;
}

function createItems(itemsCount) {
  const items = [];

  for (let i = 0; i < itemsCount; i++) {
    items.push(getItem(i));
  }

  return items;
}

function jumpTo(item) {
  if (isPaused) {
    item.classList.remove(classNames.RUNNING);
    item.classList.add(classNames.DONE);
  } else {
    item.classList.add(classNames.RUNNING);
    item.classList.remove(classNames.DONE);
  }

  let sibling = item;

  while ((sibling = sibling.previousSibling)) {
    sibling.classList.remove(classNames.RUNNING);
    sibling.classList.add(classNames.DONE);
  }

  sibling = item;

  while ((sibling = sibling.nextSibling)) {
    sibling.classList.remove(classNames.RUNNING, classNames.DONE);
  }
}

function update(index) {
  activeIndex = index;
  jumpTo(items[activeIndex]);

  // Update slide and background color
  $state.innerHTML = activeIndex + 1;
  document.body.style.backgroundColor = colors[activeIndex];
}

function prev() {
  if (activeIndex > 0) {
    update(activeIndex - 1);
  } 
  // else {
  //   // Loop to the last slide
  //   update(ITEMS_COUNT - 1);
  // }
}

function next() {
  if (activeIndex < ITEMS_COUNT - 1) {
    update(activeIndex + 1);
  } 
  // else {
  //   // Loop to the first slide
  //   update(0);
  // }
}

function playPause() {
  $pagination.classList.toggle(classNames.PAUSED);
  isPaused = !isPaused;

  // When unpausing,
  // if the current slide is done, jump to the next one
  if (!isPaused && items[activeIndex].classList.contains(classNames.DONE)) {
    next();
  }
}

const colors = ['#98ede0', '#74b9ff', '#a29bfe', '#fd79a8', '#ffeaa7'];

const classNames = {
  RUNNING: 'pagination-item--running',
  DONE: 'pagination-item--done',
  PAUSED: 'pagination--paused',
};

let activeIndex = 0;
let isPaused = false;

const ITEMS_COUNT = 5;
const items = createItems(ITEMS_COUNT);

const $pagination = document.querySelector('.pagination');
const $state = document.querySelector('.state');
const $prev = document.querySelector('.control--prev');
const $next = document.querySelector('.control--next');
const $playPause = document.querySelector('.control--play-pause');

$pagination.replaceChildren(...items);

$prev.addEventListener('click', prev);
$next.addEventListener('click', next);

$playPause.addEventListener('click', playPause);

update(activeIndex);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.