JSFiddle - React, Tailwind, and code Playground
by Kronos11
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="styles.css">
<script src="script.js" defer></script>
</head>
<body>
<div class="row">
<div class="header">
<h3 class="title">A Model Family</h3>
<div class="progress-bar"></div>
</div>
<div class="container">
<button class="handle left-handle">
<div class="text">‹</div>
</button>
<div class="slider">
<a href="https://www.youtube.com/watch?v=-v8-jebvHtY">
<img src="https://streamwon.com/wp-content/uploads/2024/03/Blue-and-Yello-intro.jpg">
</a>
</div>
<div class="slider">
<a href="https://www.youtube.com/watch?v=Gm7Zzq-3C5w">
<img src="https://streamwon.com/wp-content/uploads/2024/03/Winter-Wonderland.jpg">
</a>
</div>
<div class="slider">
<a href="https://www.youtube.com/watch?v=VGLPtOx1KkU">
<img src="https://streamwon.com/wp-content/uploads/2024/03/A-Day-in-the-Life.jpg">
</a>
</div>
<div class="slider">
<a href="https://www.youtube.com/watch?v=owBPlBYKK3c">
<img src="https://streamwon.com/wp-content/uploads/2024/03/A-Pirate-Family.jpg">
</a>
</div>
<div class="slider">
<a href="https://www.youtube.com/watch?v=szVDwW0rML8">
<img src="https://streamwon.com/wp-content/uploads/2024/03/Free-People-intro.jpg">
</a>
</div>
<div class="slider">
<a href="https://www.youtube.com/watch?v=slENv411Xiw">
<img src="https://streamwon.com/wp-content/uploads/2024/03/Plant-Bomb.jpg">
</a>
</div>
<button class="handle right-handle">
<div class="text">›</div>
</button>
...
CSS
*, *::after, *::before {
box-sizing: border-box;
}
:root {
--handle-size: 3rem;
--img-gap: 0.25rem;
}
body {
margin: 0;
}
.container a {
display: flex;
justify-content: center;
overflow: hidden;
}
.slider a {
--items-per-screen: 4;
--slider-index: 0;
display: flex;
flex-grow: 1;
margin: 0 var(--img-gap);
transform: translateX(calc(var(--slider-index) * -100%));
transition: transform 250ms ease-in-out;
}
.slider > a img {
flex: 0 0 calc(100% / var(--items-per-screen));
max-width: calc(100% / var(--items-per-screen));
aspect-ratio: 16 / 9;
padding: var(--img-gap);
border-radius: 1rem;
}
.handle {
border: none;
border-radius: 1rem;
flex-grow: 0;
flex-shrink: 0;
background-color: rgba(255, 255, 255, 0.25);
z-index: 10;
margin: var(--img-gap) 0;
width: var(--handle-size);
cursor: pointer;
font-size: 5rem;
display: flex;
align-items: center;
justify-content: center;
color: white;
line-height: 0;
transition: background-color 150ms ease-in-out;
}
.left-handle {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.right-handle {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.handle:hover,
.handle:focus {
background-color: rgba(255, 255, 255, 0.5);
}
.text {
transition: transform 150ms ease-in-out;
}
.handle:hover .text,
.handle:focus .text {
transform: scale(1.2);
}
.header {
display: flex;
justify-content: space-between;
padding: 0.5rem calc(var(--img-gap) * 2 + var(--handle-size));
align-items: center;
}
.title {
font-size: 2rem;
margin: 0;
}
.progress-bar {
display: flex;
gap: 0.25rem;
}
.progress-item {
flex: 0 0 1.5rem;
min-width: 1.5rem;
height: 0.5rem;
background-color: rgba(255, 255, 255, 0.5);
}
.progress-item.active {
background-color: rgba(255, 255, 255, 0.9);
}
@media (max-width: 1000px) {
.slider {
--items-per-screen: 3;
}
}
@media (max-width: 500px) {
.slider {
--items-per-screen: 2;
}
}
body...
JavaScript
document.addEventListener("click", e => {
let handle
if (e.target.matches(".handle")) {
handle = e.target
} else {
handle = e.target.closest(".handle")
}
if (handle != null) onHandleClick(handle)
})
const throttleProgressBar = throttle(() => {
document.querySelectorAll(".progress-bar").forEach(calculateProgressBar)
}, 250)
window.addEventListener("resize", throttleProgressBar)
document.querySelectorAll(".progress-bar").forEach(calculateProgressBar)
function calculateProgressBar(progressBar) {
progressBar.innerHTML = ""
const slider = progressBar.closest(".row").querySelector(".slider")
const itemCount = slider.children.length
const itemsPerScreen = parseInt(
getComputedStyle(slider).getPropertyValue("--items-per-screen")
)
let sliderIndex = parseInt(
getComputedStyle(slider).getPropertyValue("--slider-index")
)
const progressBarItemCount = Math.ceil(itemCount / itemsPerScreen)
if (sliderIndex >= progressBarItemCount) {
slider.style.setProperty("--slider-index", progressBarItemCount - 1)
sliderIndex = progressBarItemCount - 1
}
for (let i = 0; i < progressBarItemCount; i++) {
const barItem = document.createElement("div")
barItem.classList.add("progress-item")
if (i === sliderIndex) {
barItem.classList.add("active")
}
progressBar.append(barItem)
}
}
function onHandleClick(handle) {
const progressBar = handle.closest(".row").querySelector(".progress-bar")
const slider = handle.closest(".container").querySelector(".slider")
const sliderIndex = parseInt(
getComputedStyle(slider).getPropertyValue("--slider-index")
)
const progressBarItemCount = progressBar.children.length
if (handle.classList.contains("left-handle")) {
if (sliderIndex - 1 < 0) {
slider.style.setProperty("--slider-index", progressBarItemCount - 1)
progressBar.children[sliderIndex].classList.remove("active")
progressBar.children[progressBarItemCount -...