Photography — Testimonials
A gallery-dark testimonials section for a photographer's site: quote cards paired with client portraits, five-star ratings, and an auto-advancing carousel with dot navigation, pause-on-hover, and keyboard controls. Thin uppercase labels, a Fraunces display serif, and full-bleed portrait imagery give it an editorial, image-first feel that keeps the focus on real client voices.
MCP
Code
:root {
--ink: #0b0b0c;
--bg: #101012;
--surface: #17171b;
--surface-2: #1e1e23;
--paper: #f6f4ef;
--sand: #c9b79c;
--sand-d: #a8916f;
--muted: #9a9aa2;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--paper);
background:
radial-gradient(1200px 600px at 80% -10%, rgba(201, 183, 156, 0.08), transparent 60%),
var(--bg);
min-height: 100vh;
}
.page {
max-width: 1040px;
margin: 0 auto;
padding: clamp(32px, 6vw, 80px) 20px;
}
/* ---------- Header ---------- */
.testimonials {
position: relative;
}
.testimonials__head {
text-align: center;
max-width: 620px;
margin: 0 auto clamp(28px, 5vw, 48px);
}
.kicker {
margin: 0 0 14px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--sand);
}
.testimonials__title {
font-family: "Fraunces", Georgia, serif;
font-weight: 400;
font-size: clamp(28px, 5vw, 46px);
line-height: 1.08;
margin: 0 0 16px;
letter-spacing: -0.01em;
}
.testimonials__sub {
margin: 0;
color: var(--muted);
font-size: 15px;
}
/* ---------- Carousel shell ---------- */
.carousel {
position: relative;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 12px;
}
.track {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
min-height: 380px;
box-shadow: 0 30px 70px -40px rgba(0, 0, 0, 0.9);
}
/* ---------- Slide ---------- */
.slide {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: 42% 1fr;
opacity: 0;
transform: translateY(14px) scale(0.99);
transition: opacity 0.6s ease, transform 0.6s ease;
pointer-events: none;
}
.slide.is-active {
position: relative;
opacity: 1;
transform: none;
pointer-events: auto;
}
.slide[hidden] {
display: grid;
}
.slide__photo {
position: relative;
margin: 0;
overflow: hidden;
}
.slide__photo img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
filter: saturate(0.92) contrast(1.02);
transition: transform 8s ease;
}
.slide.is-active .slide__photo img {
transform: scale(1.06);
}
.slide__photo::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent 60%, rgba(23, 23, 27, 0.55) 100%);
pointer-events: none;
}
.slide__badge {
position: absolute;
top: 16px;
left: 16px;
z-index: 2;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ink);
background: var(--sand);
padding: 6px 12px;
border-radius: 999px;
}
.slide__body {
padding: clamp(28px, 4vw, 46px);
display: flex;
flex-direction: column;
justify-content: center;
gap: 22px;
}
/* ---------- Stars ---------- */
.stars {
display: inline-flex;
gap: 4px;
}
.stars span {
width: 18px;
height: 18px;
background: var(--sand);
-webkit-mask: var(--star) center / contain no-repeat;
mask: var(--star) center / contain no-repeat;
--star: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2l2.9 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14l-5-4.87 7.1-1.01z'/%3E%3C/svg%3E");
filter: drop-shadow(0 2px 6px rgba(201, 183, 156, 0.3));
}
/* ---------- Quote ---------- */
.quote {
font-family: "Fraunces", Georgia, serif;
font-weight: 400;
font-size: clamp(19px, 2.4vw, 25px);
line-height: 1.4;
margin: 0;
color: var(--paper);
letter-spacing: -0.005em;
}
.cite {
display: flex;
flex-direction: column;
gap: 3px;
}
.cite__name {
font-weight: 700;
font-size: 15px;
letter-spacing: 0.01em;
}
.cite__role {
font-size: 12px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
}
/* ---------- Nav arrows ---------- */
.nav {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: 50%;
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--paper);
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.15s ease,
color 0.2s ease;
}
.nav:hover {
background: var(--sand);
border-color: var(--sand);
color: var(--ink);
}
.nav:active {
transform: scale(0.92);
}
.nav:focus-visible {
outline: 2px solid var(--sand);
outline-offset: 3px;
}
/* ---------- Footer / dots ---------- */
.testimonials__foot {
display: flex;
align-items: center;
justify-content: center;
gap: 22px;
margin-top: 26px;
}
.dots {
display: inline-flex;
gap: 10px;
}
.dot {
width: 9px;
height: 9px;
padding: 0;
border: 0;
border-radius: 50%;
background: var(--line-2);
cursor: pointer;
transition: background 0.25s ease, transform 0.25s ease, width 0.25s ease;
}
.dot:hover {
background: var(--sand-d);
}
.dot.is-active {
width: 26px;
border-radius: 999px;
background: var(--sand);
}
.dot:focus-visible {
outline: 2px solid var(--sand);
outline-offset: 3px;
}
.playpause {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: inherit;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
background: transparent;
border: 1px solid var(--line);
border-radius: 999px;
padding: 7px 14px;
cursor: pointer;
transition: color 0.2s ease, border-color 0.2s ease;
}
.playpause:hover {
color: var(--paper);
border-color: var(--line-2);
}
.playpause:focus-visible {
outline: 2px solid var(--sand);
outline-offset: 3px;
}
.playpause__icon {
width: 10px;
height: 10px;
}
.playpause__icon[data-state="playing"] {
/* two vertical bars (pause glyph) shown while playing */
background: linear-gradient(var(--sand), var(--sand)) left / 3px 100% no-repeat,
linear-gradient(var(--sand), var(--sand)) right / 3px 100% no-repeat;
}
.playpause__icon[data-state="paused"] {
/* triangle (play glyph) shown while paused */
background: var(--sand);
clip-path: polygon(0 0, 100% 50%, 0 100%);
}
/* ---------- Progress bar ---------- */
.progress {
height: 2px;
margin-top: 20px;
background: var(--line);
border-radius: 999px;
overflow: hidden;
}
.progress span {
display: block;
height: 100%;
width: 0;
background: linear-gradient(90deg, var(--sand-d), var(--sand));
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
background: var(--paper);
color: var(--ink);
font-size: 13px;
font-weight: 600;
padding: 10px 18px;
border-radius: 999px;
box-shadow: 0 20px 40px -20px rgba(0, 0, 0, 0.8);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 720px) {
.carousel {
grid-template-columns: 1fr;
}
.nav {
position: absolute;
top: 130px;
z-index: 5;
}
.nav--prev {
left: 12px;
}
.nav--next {
right: 12px;
}
.slide {
grid-template-columns: 1fr;
grid-template-rows: 260px auto;
}
.slide__photo::after {
background: linear-gradient(0deg, rgba(23, 23, 27, 0.7) 0%, transparent 45%);
}
.track {
min-height: 0;
}
}
@media (max-width: 520px) {
.page {
padding: 40px 14px 60px;
}
.slide {
grid-template-rows: 220px auto;
}
.slide__body {
padding: 24px 20px 28px;
gap: 16px;
}
.quote {
font-size: 18px;
}
.nav {
width: 40px;
height: 40px;
top: 100px;
}
.testimonials__foot {
gap: 16px;
flex-wrap: wrap;
}
}
@media (prefers-reduced-motion: reduce) {
.slide,
.slide__photo img,
.progress span {
transition: none !important;
}
}(function () {
"use strict";
var INTERVAL = 6000; // ms per slide
var carousel = document.getElementById("carousel");
var track = document.getElementById("track");
var slides = Array.prototype.slice.call(track.querySelectorAll(".slide"));
var dotsWrap = document.getElementById("dots");
var prevBtn = document.getElementById("prev");
var nextBtn = document.getElementById("next");
var playBtn = document.getElementById("playpause");
var playIcon = playBtn.querySelector(".playpause__icon");
var progressBar = document.getElementById("progressbar");
var toastEl = document.getElementById("toast");
var index = 0;
var playing = true;
var paused = false; // hover pause
var rafId = null;
var startTs = 0;
var elapsedBefore = 0;
var reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
/* ---------- Toast ---------- */
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 1800);
}
/* ---------- Build dots ---------- */
slides.forEach(function (slide, i) {
var dot = document.createElement("button");
dot.className = "dot" + (i === 0 ? " is-active" : "");
dot.type = "button";
dot.setAttribute("role", "tab");
dot.setAttribute("aria-label", "Go to testimonial " + (i + 1));
dot.setAttribute("aria-selected", i === 0 ? "true" : "false");
dot.addEventListener("click", function () {
goTo(i);
resetTimer();
});
dotsWrap.appendChild(dot);
});
var dots = Array.prototype.slice.call(dotsWrap.children);
/* ---------- Show a slide ---------- */
function goTo(next) {
if (next === index) return;
var total = slides.length;
next = ((next % total) + total) % total;
slides[index].classList.remove("is-active");
slides[index].setAttribute("hidden", "");
dots[index].classList.remove("is-active");
dots[index].setAttribute("aria-selected", "false");
index = next;
slides[index].removeAttribute("hidden");
slides[index].classList.add("is-active");
dots[index].classList.add("is-active");
dots[index].setAttribute("aria-selected", "true");
}
function step(dir) {
goTo(index + dir);
}
/* ---------- Progress / auto-advance ---------- */
function frame(ts) {
if (!startTs) startTs = ts;
var elapsed = elapsedBefore + (ts - startTs);
var pct = Math.min(elapsed / INTERVAL, 1);
progressBar.style.width = (pct * 100).toFixed(2) + "%";
if (pct >= 1) {
step(1);
resetTimer();
return;
}
rafId = requestAnimationFrame(frame);
}
function startTimer() {
if (reduceMotion) return; // no autoplay when reduced motion
cancelAnimationFrame(rafId);
startTs = 0;
rafId = requestAnimationFrame(frame);
}
function resetTimer() {
elapsedBefore = 0;
progressBar.style.width = "0%";
if (playing && !paused) startTimer();
}
function stopTimer() {
cancelAnimationFrame(rafId);
rafId = null;
}
/* Pause on hover: freeze the elapsed time, resume from there */
function freeze() {
if (!playing || paused) return;
paused = true;
if (startTs) elapsedBefore += performance.now() - startTs;
stopTimer();
}
function unfreeze() {
if (!playing || !paused) return;
paused = false;
if (!reduceMotion) startTimer();
}
/* ---------- Play / pause toggle ---------- */
function setPlaying(on) {
playing = on;
playBtn.setAttribute("aria-pressed", on ? "true" : "false");
playBtn.setAttribute("aria-label", on ? "Pause auto-play" : "Start auto-play");
playIcon.setAttribute("data-state", on ? "playing" : "paused");
if (on) {
paused = false;
resetTimer();
toast("Auto-play on");
} else {
stopTimer();
toast("Auto-play paused");
}
}
/* ---------- Events ---------- */
prevBtn.addEventListener("click", function () {
step(-1);
resetTimer();
});
nextBtn.addEventListener("click", function () {
step(1);
resetTimer();
});
playBtn.addEventListener("click", function () {
setPlaying(!playing);
});
carousel.addEventListener("mouseenter", freeze);
carousel.addEventListener("mouseleave", unfreeze);
carousel.addEventListener("focusin", freeze);
carousel.addEventListener("focusout", unfreeze);
// Keyboard: arrows navigate when the carousel region has focus
carousel.setAttribute("tabindex", "0");
carousel.addEventListener("keydown", function (e) {
if (e.key === "ArrowLeft") {
e.preventDefault();
step(-1);
resetTimer();
} else if (e.key === "ArrowRight") {
e.preventDefault();
step(1);
resetTimer();
} else if (e.key === " " || e.key === "Enter") {
if (e.target === carousel) {
e.preventDefault();
setPlaying(!playing);
}
}
});
// Pause when tab hidden, resume when visible
document.addEventListener("visibilitychange", function () {
if (document.hidden) {
stopTimer();
} else if (playing && !paused) {
startTimer();
}
});
/* ---------- Init ---------- */
slides.forEach(function (s, i) {
if (i !== 0) s.setAttribute("hidden", "");
});
startTimer();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Client Testimonials — Aperture Studio</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fraunces:wght@400;600&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<section
class="testimonials"
aria-roledescription="carousel"
aria-label="Client testimonials"
>
<header class="testimonials__head">
<p class="kicker">Kind words</p>
<h1 class="testimonials__title">Trusted by the moments that matter</h1>
<p class="testimonials__sub">
A few notes from couples, families, and founders I've had the joy of
photographing.
</p>
</header>
<div class="carousel" id="carousel" aria-live="polite">
<button
class="nav nav--prev"
id="prev"
type="button"
aria-label="Previous testimonial"
>
<svg viewBox="0 0 24 24" aria-hidden="true" width="20" height="20">
<path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="track" id="track">
<!-- Slide 1 -->
<article class="slide is-active" role="group" aria-roledescription="slide" aria-label="1 of 5">
<figure class="slide__photo">
<img
src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&w=900&q=80"
alt="Portrait of Mara Delacroix"
loading="lazy"
/>
<figcaption class="slide__badge">Wedding</figcaption>
</figure>
<div class="slide__body">
<div class="stars" role="img" aria-label="Rated 5 out of 5 stars">
<span></span><span></span><span></span><span></span><span></span>
</div>
<blockquote class="quote">
“She didn't just take photos — she caught the way my grandmother
laughed during the vows. We relive that whole day every time we
open the album.”
</blockquote>
<div class="cite">
<span class="cite__name">Mara Delacroix</span>
<span class="cite__role">Married in Ravello, Italy</span>
</div>
</div>
</article>
<!-- Slide 2 -->
<article class="slide" role="group" aria-roledescription="slide" aria-label="2 of 5" hidden>
<figure class="slide__photo">
<img
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=900&q=80"
alt="Portrait of Julian Okafor"
loading="lazy"
/>
<figcaption class="slide__badge">Editorial</figcaption>
</figure>
<div class="slide__body">
<div class="stars" role="img" aria-label="Rated 5 out of 5 stars">
<span></span><span></span><span></span><span></span><span></span>
</div>
<blockquote class="quote">
“Our launch shoot needed to feel expensive and honest at once.
Every frame landed. Investors literally asked who our creative
director was.”
</blockquote>
<div class="cite">
<span class="cite__name">Julian Okafor</span>
<span class="cite__role">Founder, Northlight Coffee</span>
</div>
</div>
</article>
<!-- Slide 3 -->
<article class="slide" role="group" aria-roledescription="slide" aria-label="3 of 5" hidden>
<figure class="slide__photo">
<img
src="https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=900&q=80"
alt="Portrait of Priya Anand"
loading="lazy"
/>
<figcaption class="slide__badge">Family</figcaption>
</figure>
<div class="slide__body">
<div class="stars" role="img" aria-label="Rated 5 out of 5 stars">
<span></span><span></span><span></span><span></span><span></span>
</div>
<blockquote class="quote">
“Two toddlers, zero patience, and somehow she got a portrait
that's now hanging in three grandparents' homes. Pure magic and
infinite calm.”
</blockquote>
<div class="cite">
<span class="cite__name">Priya Anand</span>
<span class="cite__role">Family session, Lisbon</span>
</div>
</div>
</article>
<!-- Slide 4 -->
<article class="slide" role="group" aria-roledescription="slide" aria-label="4 of 5" hidden>
<figure class="slide__photo">
<img
src="https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=crop&w=900&q=80"
alt="Portrait of Sebastian Vance"
loading="lazy"
/>
<figcaption class="slide__badge">Brand</figcaption>
</figure>
<div class="slide__body">
<div class="stars" role="img" aria-label="Rated 5 out of 5 stars">
<span></span><span></span><span></span><span></span><span></span>
</div>
<blockquote class="quote">
“I've booked a dozen photographers over my career. None of them
understood light — or a nervous CEO — the way this studio does.
Effortless from call to delivery.”
</blockquote>
<div class="cite">
<span class="cite__name">Sebastian Vance</span>
<span class="cite__role">Executive headshots, Berlin</span>
</div>
</div>
</article>
<!-- Slide 5 -->
<article class="slide" role="group" aria-roledescription="slide" aria-label="5 of 5" hidden>
<figure class="slide__photo">
<img
src="https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?auto=format&fit=crop&w=900&q=80"
alt="Portrait of Elena Ruiz"
loading="lazy"
/>
<figcaption class="slide__badge">Engagement</figcaption>
</figure>
<div class="slide__body">
<div class="stars" role="img" aria-label="Rated 5 out of 5 stars">
<span></span><span></span><span></span><span></span><span></span>
</div>
<blockquote class="quote">
“We're the most camera-shy people alive, and we forgot the lens
was even there. The photos feel like us — a little wild, deeply
in love.”
</blockquote>
<div class="cite">
<span class="cite__name">Elena Ruiz</span>
<span class="cite__role">Engagement, Big Sur coast</span>
</div>
</div>
</article>
</div>
<button
class="nav nav--next"
id="next"
type="button"
aria-label="Next testimonial"
>
<svg viewBox="0 0 24 24" aria-hidden="true" width="20" height="20">
<path d="M9 5l7 7-7 7" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<footer class="testimonials__foot">
<div class="dots" id="dots" role="tablist" aria-label="Choose testimonial"></div>
<button class="playpause" id="playpause" type="button" aria-label="Pause auto-play" aria-pressed="true">
<span class="playpause__icon" data-state="playing" aria-hidden="true"></span>
<span class="playpause__label">Auto</span>
</button>
</footer>
<div class="progress" aria-hidden="true"><span id="progressbar"></span></div>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A dark, editorial testimonials block built for a portrait and wedding photographer. Each slide pairs a full-bleed client portrait with a large Fraunces pull-quote, a five-star rating, and the client’s name, role, and shoot type. The layout leans into gallery whitespace, thin uppercase kicker labels, and warm sand accents drawn straight from the photography palette.
The carousel auto-advances every few seconds and loops seamlessly. Readers can jump to any testimonial with the dot navigation, step through with the previous and next arrows, or use the left and right arrow keys once the region is focused. Hovering the card pauses the timer so nobody loses their place mid-read, and a slim progress bar shows how long until the next slide.
Everything is vanilla HTML, CSS, and JavaScript with no dependencies. Slides cross-fade with a subtle lift, controls expose proper aria labels and visible focus rings, and the whole component reflows to a single-column, image-on-top layout below 520px so it stays legible on phones.