<div class="slideshow">
<div class="slides-container">
<div class="slides">
<div class="slide"><img src="https://picsum.photos/600/300?v=1" alt=""></div>
<div class="slide"><img src="https://picsum.photos/600/300?v=2" alt=""></div>
<div class="slide"><img src="https://picsum.photos/600/300?v=3" alt=""></div>
<div class="slide"><img src="https://picsum.photos/600/300?v=4" alt=""></div>
<div class="slide"><img src="https://picsum.photos/600/300?v=5" alt=""></div>
</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>
*,
*:before,
*:after {
box-sizing: border-box;
}
:root {
--min-margin-x: 0px;
--max-size: 600px;
--size: min(var(--max-size), 100vw - var(--min-margin-x));
--duration: 1s;
--easing: ease;
--bullet-size: 12px;
}
body {
perspective: 1000px;
display: grid;
place-items: center;
height: 100vh;
--3d: 0;
&.is3d {
--3d: 1;
}
}
.slideshow {
display: grid;
grid-template-rows: 1fr auto;
grid-template-columns: 1fr;
gap: 10px;
flex-direction: column;
width: var(--size);
transform-style: preserve-3d;
transform: rotateY(calc(60deg * var(--3d))) rotateZ(calc(-5deg * var(--3d)));
transition: transform 1s ease;
}
.slides-container {
grid-column: 1;
grid-row: 1;
box-shadow: 0 0 0 30000px var(--shadow, #fff);
transition: box-shadow 1s ease;
width: var(--size);
border-radius: clamp(
0px,
(100vw - var(--max-size) - var(--min-margin-x)) * 999,
15px
);
transform-style: preserve-3d;
.is3d & {
overflow: visible;
--shadow: rgba(0,0,0,0.5);
}
}
.slides {
display: flex;
transform: translateX(calc(-1 * var(--size) * var(--slide, 0))) translateZ(calc((-150px * var(--3d)) - 0.1px));
transition: transform var(--duration) var(--easing);
transform-style: preserve-3d;
}
.slide {
font-size: 0;
}
img {
width: var(--size);
}
.buttons {
transform-style: preserve-3d;
transform: translateZ(calc(100px * var(--3d)));
transition: transform 1s ease;
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
grid-column: 1;
grid-row: 1;
pointer-events: none;
button {
pointer-events: all;
width: 40px;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
color: #000;
border-radius: 100vw;
border: 1px solid #000;
background: #fff;
transition: all 0.2s ease;
cursor: pointer;
opacity: 0.4;
&:hover {
box-shadow: 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.24);
opacity: 0.8;
}
}
}
.bullets {
transform-style: preserve-3d;
transform: translateZ(calc(100px * var(--3d)));
transition: transform 1s ease;
grid-column: 1;
grid-row: 2;
display: flex;
justify-content: center;
align-items: center;
gap: var(--bullet-size);
place-self: center;
height: var(--bullet-size);
.bullet {
aspect-ratio: 1;
width: var(--bullet-size);
background: #333;
border-radius: 100vw;
cursor: pointer;
&.active {
box-shadow: inset 0 0 0 2px #000;
background: #fff;
}
}
}
View Compiled
window.addEventListener("load", () => {
// 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;
// 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[Reflect.get(obj, prop)].classList.remove("active");
bullets[value].classList.add("active");
}
// Actually set the active.slide prop to it's new value
return Reflect.set(obj, prop, value)
}
}
);
// 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
// within the proxy handler for active.slide.
const bullets = bulletContainer.querySelectorAll(".bullet");
// Add the prev/next functionality to the buttons
prev.addEventListener("click", () => active.slide--);
next.addEventListener("click", () => active.slide++);
document.addEventListener('keydown', e => {
if (e.key == ' ') {
e.preventDefault();
document.body.classList.toggle('is3d')
}
});
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.