A 3D coverflow image carousel built with CSS 3D transforms and a small vanilla JavaScript controller. The active photo faces the viewer while the neighbours turn away on both sides, sink back in depth and darken, with a soft mirror reflection below. Use it for photo journals, travel destinations, album covers or portfolio highlights.
How it works
The root section.cf-carousel holds a heading, the .cf-stage, a caption area and the controls. .cf-stage sets perspective, and the .cf-track list uses transform-style: preserve-3d. Every .cf-item is absolutely centered and placed by three custom properties: --cf-offset (distance from the active slide, e.g. -2), --cf-abs (its absolute value) and --cf-dir (-1, 0 or 1). From those the CSS builds translateX(), a negative translateZ() and a rotateY(), and lowers brightness() for each step away. -webkit-box-reflect draws the reflection in Chromium and Safari.
The script runs on DOMContentLoaded for every .cf-carousel. It reads the starting slide from data-start, creates one .cf-dot button per slide, and on every change writes the three properties onto each item, toggles .is-active and .is-far, and copies data-title and data-place of the active slide into .cf-caption-title and .cf-caption-place. Slides react to clicks, the .cf-prev and .cf-next buttons step through the list, and pointerdown/pointerup on the stage turn a horizontal swipe of more than 40px into a step. The transition on .cf-item animates the turn. Until the script adds .is-ready, the track falls back to a horizontal scroll-snap row, so the photos stay usable without JavaScript.
Customization
--cf-angleon.cf-carousel: how far the side photos turn, e.g.70degfor a stronger fold.--cf-spacing: the horizontal step between slides; it is set again in the 640px and 960px media queries.--cf-slide-wand--cf-slide-h: the photo size per breakpoint.--cf-depthand--cf-perspective: how far side slides sink back and how strong the 3D effect looks.--cf-accent: the color of the kicker, the active dot and the button hover state.data-starton.cf-carousel: the zero-based index of the slide shown first.VISIBLE_RANGEinscript.js: slides further away than this get.is-farand fade out, which helps with long galleries.data-titleanddata-placeon each.cf-item: the caption text shown under the active photo.
Accessibility
The section uses aria-roledescription="carousel" and each item is a group with aria-roledescription="slide" and a label such as "4 of 7". Inactive slides get aria-hidden, so screen readers only read the visible photo, and the caption is a polite live region. Arrow buttons and dots are real buttons with labels, the active dot has aria-current, and ArrowLeft, ArrowRight, Home and End work while focus is inside the carousel. Focus rings use the accent color. With prefers-reduced-motion: reduce slides switch without animation.
Usage notes
The photos come from Unsplash; replace them with your own images and keep a portrait crop. Several carousels can share a page, as each one is scoped to its own root.