Hello, I have one page on my site (below) I would like to create a Coverflow slider to showcase my photograph gallery as opposed to using thumbnails. The only option I saw for this was the Swiper Bric. Are there any other options to implement this effect? Thanks for your time.
I use Iconic Gallery
Thank you for the reply. I use that for my galleries too and is excellent. What I am referring to are the thumbnails I use on my gallery page to take you to each individual gallery. I would like to use a Coverflow effect as opposed to the thumbnails.
Playing with CodePen
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Coverflow — Fixed Active Expand + Smooth Pop + Responsive</title>
<!-- Swiper CSS (CDN) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.css">
<style>
:root{
/* Responsive sizing + animation knobs */
--slideW: 320px;
/* Active stays at this scale (fixed expanded mode) */
--activeScale: 1.28;
/* Temporary overshoot multiplier when it becomes active */
--popExtra: 1.14;
/* Pop duration in ms */
--popMs: 460;
}
/* Bootstrap-ish breakpoints */
@media (max-width: 991.98px){
:root{ --slideW: 290px; --activeScale: 1.24; --popExtra: 1.13; --popMs: 440; }
}
@media (max-width: 767.98px){
:root{ --slideW: 250px; --activeScale: 1.18; --popExtra: 1.12; --popMs: 420; }
}
@media (max-width: 575.98px){
:root{ --slideW: 210px; --activeScale: 1.12; --popExtra: 1.10; --popMs: 380; }
}
html, body { height: 100%; margin: 0; }
body{
display:grid;
place-items:center;
background:#0b0f19;
font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
overflow-x:hidden;
}
.my-coverflow{
width:min(1100px,92vw);
padding:44px 0 70px;
}
.my-coverflow .swiper-slide{ width: var(--slideW); cursor: default; }
.my-coverflow .swiper-slide-active{ cursor: zoom-in; }
/* We animate the IMG (Swiper owns slide transforms for coverflow) */
.my-coverflow .swiper-slide img{
width:100%;
display:block;
border-radius:16px;
box-shadow:0 18px 60px rgba(0,0,0,.50);
user-select:none;
-webkit-user-drag:none;
/* JS drives these; defaults for non-active far slides */
opacity:.08;
transform:translateZ(0) scale(.78);
filter:blur(2.4px) saturate(.72) contrast(.90);
transition:
transform .95s cubic-bezier(.18,.90,.20,1), /* smoother pop/settle */
opacity .55s ease,
filter .55s ease;
}
.my-coverflow .swiper-button-prev,
.my-coverflow .swiper-button-next{ color:#fff; }
.my-coverflow .swiper-pagination-bullet{ opacity:.25; }
.my-coverflow .swiper-pagination-bullet-active{ opacity:1; }
/* Lightbox overlay */
.lightbox{
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(0,0,0,.78);
z-index: 9999;
padding: 24px;
}
.lightbox.is-open{ display:flex; }
.lightbox__panel{
position: relative;
max-width: min(1100px, 96vw);
max-height: 92vh;
}
.lightbox__img{
display:block;
max-width: 100%;
max-height: 92vh;
border-radius: 16px;
box-shadow: 0 24px 90px rgba(0,0,0,.65);
transform-origin: center center;
transform: scale(2);
transition: transform .25s ease;
cursor: zoom-out;
user-select:none;
-webkit-user-drag:none;
}
.lightbox__close{
position:absolute;
top:-10px;
right:-10px;
width:42px;
height:42px;
border:0;
border-radius:999px;
cursor:pointer;
background: rgba(255,255,255,.12);
color:#fff;
font-size:20px;
line-height:42px;
text-align:center;
backdrop-filter: blur(6px);
}
.lightbox__hint{
position: fixed;
left: 50%;
bottom: 18px;
transform: translateX(-50%);
color: rgba(255,255,255,.85);
font-size: 14px;
text-align:center;
pointer-events:none;
}
@media (prefers-reduced-motion: reduce){
.my-coverflow .swiper-slide img,
.lightbox__img{ transition:none; }
}
</style>
</head>
<body>
<div class="swiper my-coverflow" id="coverflow">
<div class="swiper-wrapper">
<!-- Replace these with your own images -->
<div class="swiper-slide"><img src="upload://3wddxKWGMCpzTeZVSM5B2boZPHQ.jpeg" alt="Gallery photo 1" loading="lazy"></div>
<div class="swiper-slide"><img src="upload://v7NFBFydGPpj53FkDFEolfFWh8N.jpeg" alt="Gallery photo 2" loading="lazy"></div>
<div class="swiper-slide"><img src="upload://w4hPzJHdMXI4GFHVrkV8jEOvIHP.jpeg" alt="Gallery photo 3" loading="lazy"></div>
<div class="swiper-slide"><img src="upload://vOUMW3eWnfTkC4108v8zqEvyEw.jpeg" alt="Gallery photo 4" loading="lazy"></div>
<div class="swiper-slide"><img src="upload://r0YyHNrj7whq6t2tDCIUMHkK3lO.jpeg" alt="Gallery photo 5" loading="lazy"></div>
</div>
<div class="swiper-button-prev" aria-label="Previous slide"></div>
<div class="swiper-button-next" aria-label="Next slide"></div>
<div class="swiper-pagination" aria-label="Pagination"></div>
</div>
<div class="lightbox" id="lightbox" aria-hidden="true">
<div class="lightbox__panel" role="dialog" aria-modal="true" aria-label="Expanded image">
<button type="button" class="lightbox__close" id="lbClose" aria-label="Close">×</button>
<img class="lightbox__img" id="lbImg" alt="">
</div>
<div class="lightbox__hint">Click anywhere or press Esc to close</div>
</div>
<!-- Swiper JS (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.js"></script>
<script>
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
function readCSSNumber(varName, fallback){
const v = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
const n = parseFloat(v);
return Number.isFinite(n) ? n : fallback;
}
const swiper = new Swiper(".my-coverflow", {
effect: "coverflow",
grabCursor: true,
centeredSlides: true,
slidesPerView: "auto",
loop: true,
speed: 700,
watchSlidesProgress: true,
slideToClickedSlide: true,
// Responsive coverflow tuning (Bootstrap-ish)
breakpoints: {
0: {
coverflowEffect: { rotate: 28, stretch: -34, depth: 260, modifier: 1.10, slideShadows: false }
},
576: {
coverflowEffect: { rotate: 32, stretch: -44, depth: 340, modifier: 1.15, slideShadows: false }
},
768: {
coverflowEffect: { rotate: 36, stretch: -54, depth: 420, modifier: 1.20, slideShadows: false }
},
992: {
coverflowEffect: { rotate: 38, stretch: -58, depth: 460, modifier: 1.25, slideShadows: false }
},
1200: {
coverflowEffect: { rotate: 40, stretch: -62, depth: 520, modifier: 1.28, slideShadows: false }
}
},
pagination: { el: ".my-coverflow .swiper-pagination", clickable: true },
navigation: { nextEl: ".my-coverflow .swiper-button-next", prevEl: ".my-coverflow .swiper-button-prev" },
keyboard: { enabled: true }
});
// Pop control
let popSlide = null;
let popUntil = 0;
function updateVisuals() {
const now = performance.now();
const activeScale = readCSSNumber("--activeScale", 1.24);
const popExtra = readCSSNumber("--popExtra", 1.12);
swiper.slides.forEach((slide) => {
const img = slide.querySelector("img");
if (!img) return;
const isActive = slide.classList.contains("swiper-slide-active");
const p = slide.progress || 0;
const a = clamp(Math.abs(p), 0, 3);
// Strong fade for non-active slides (very obvious)
const opacity = isActive ? 1 : clamp(1 - a * 0.78, 0.04, 0.75);
// Fixed expanded mode for active: ignore falloff scale
let scale = isActive ? activeScale : clamp(1 - a * 0.12, 0.74, 0.98);
// Smooth overshoot pop when slide becomes active
if (isActive && slide === popSlide && now < popUntil) {
scale *= popExtra;
}
img.style.opacity = opacity.toFixed(3);
img.style.transform = `translateZ(0) scale(${scale.toFixed(3)})`;
// Clear and crisp on active, more blur/desat off-center
if (isActive) {
img.style.filter = "blur(0px) saturate(1.10) contrast(1.03)";
} else {
const blur = a * 1.25;
const sat = clamp(1.05 - a * 0.28, 0.60, 1.05);
const con = clamp(1.02 - a * 0.10, 0.80, 1.02);
img.style.filter = `blur(${blur.toFixed(2)}px) saturate(${sat.toFixed(2)}) contrast(${con.toFixed(2)})`;
}
});
}
function startPop() {
const popMs = readCSSNumber("--popMs", 420);
popSlide = swiper.slides[swiper.activeIndex] || null;
popUntil = performance.now() + popMs;
updateVisuals();
clearTimeout(startPop._t);
startPop._t = setTimeout(updateVisuals, popMs + 80);
}
swiper.on("init", () => { updateVisuals(); startPop(); });
swiper.on("setTranslate", updateVisuals);
swiper.on("resize", () => { swiper.update(); updateVisuals(); });
swiper.on("slideChangeTransitionStart", startPop);
// Some embeds won’t fire init in the order you expect; force first render:
updateVisuals();
startPop();
// ===== Lightbox (click active image to expand) =====
const coverflowEl = document.getElementById("coverflow");
const lightbox = document.getElementById("lightbox");
const lbImg = document.getElementById("lbImg");
const lbClose = document.getElementById("lbClose");
let isOpen = false;
let lastActiveImg = null;
function openLightboxFromActive() {
const activeSlide = swiper.slides[swiper.activeIndex];
const activeImg = activeSlide && activeSlide.querySelector("img");
if (!activeImg) return;
lastActiveImg = activeImg;
lbImg.src = activeImg.currentSrc || activeImg.src;
lbImg.alt = activeImg.alt || "Expanded image";
lbImg.style.transform = "scale(2)";
lightbox.classList.add("is-open");
lightbox.setAttribute("aria-hidden", "false");
isOpen = true;
}
function closeLightbox() {
if (!isOpen) return;
lightbox.classList.remove("is-open");
lightbox.setAttribute("aria-hidden", "true");
lbImg.removeAttribute("src");
isOpen = false;
if (lastActiveImg) {
try { lastActiveImg.scrollIntoView({ block: "nearest", inline: "center" }); } catch(e) {}
}
}
coverflowEl.addEventListener("click", (e) => {
if (isOpen) return;
const slide = e.target.closest(".swiper-slide");
if (!slide) return;
if (!slide.classList.contains("swiper-slide-active")) return;
openLightboxFromActive();
});
lbClose.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
closeLightbox();
});
lightbox.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
closeLightbox();
});
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && isOpen) closeLightbox();
});
</script>
</body>
</html>
Thank you @KBConcepts
Miles and miles of code for a simple thing.
But anyway … great job KBConcept.
Thank you
Hi @Derek - I use coverflow on a few projects, but not sure if its the right thing to be using for what you are looking for in my personal opinion.
I would opt for keeping it simple and use horizontal scroll so, I find it looks much better and the user can scroll easier and no messing - its all in Blocs ready to do.
But thats just my way and not saying it’s right ! - but good luck on what you do and please post your results.
Good luck
I know, I was just playing in the sandbox. ![]()