Portfolio — Testimonial / Recommendation Block
A polished portfolio testimonial section for a fictional product designer: a featured quote card with a CSS-gradient avatar, name, role and company, a small inline-SVG company logo and a star rating, wrapped in a carousel of four recommendations. Vanilla JS drives a pausable auto-advancing slider with arrow, dot and keyboard navigation, swipe support, hover and focus pausing, and smooth slide transitions. Neutral Inter and Newsreader base, accessible with live regions and visible focus rings, fully responsive.
MCP
Code
:root {
--ink: #15151b;
--ink-soft: #4a4a57;
--ink-faint: #7b7b88;
--paper: #fbfbfd;
--surface: #ffffff;
--line: #e7e7ee;
--line-strong: #d6d6e0;
--accent: #4f46e5;
--accent-soft: #eef0ff;
--accent-ink: #3730a3;
--star: #f5a623;
--star-empty: #d7d7e0;
--shadow-sm: 0 1px 2px rgba(21, 21, 27, 0.06);
--shadow-md: 0 18px 50px -20px rgba(21, 21, 27, 0.28),
0 6px 18px -10px rgba(21, 21, 27, 0.16);
--radius: 22px;
--ease: cubic-bezier(0.22, 0.61, 0.36, 1);
--serif: "Newsreader", Georgia, "Times New Roman", serif;
--sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(1.25rem, 4vw, 3.5rem);
font-family: var(--sans);
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 600px at 85% -10%, var(--accent-soft), transparent 60%),
radial-gradient(900px 500px at -10% 110%, #f4f1ff, transparent 55%),
var(--paper);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.skip-link {
position: absolute;
left: 1rem;
top: -3rem;
z-index: 20;
padding: 0.6rem 1rem;
border-radius: 999px;
background: var(--ink);
color: #fff;
font-weight: 600;
font-size: 0.85rem;
text-decoration: none;
transition: top 0.2s var(--ease);
}
.skip-link:focus {
top: 1rem;
}
.wrap {
width: 100%;
max-width: 720px;
}
/* ---------- Section header ---------- */
.recs__head {
text-align: center;
margin-bottom: clamp(1.5rem, 4vw, 2.4rem);
}
.recs__eyebrow {
margin: 0 0 0.5rem;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent-ink);
}
.recs__title {
margin: 0 0 0.6rem;
font-family: var(--serif);
font-weight: 500;
font-size: clamp(2rem, 6vw, 2.9rem);
letter-spacing: -0.01em;
line-height: 1.08;
}
.recs__lede {
margin: 0 auto;
max-width: 46ch;
color: var(--ink-soft);
font-size: clamp(0.95rem, 2.4vw, 1.05rem);
}
/* ---------- Carousel shell ---------- */
.carousel {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: clamp(0.5rem, 2vw, 1rem);
}
.carousel__viewport {
overflow: hidden;
border-radius: var(--radius);
}
.carousel__track {
list-style: none;
margin: 0;
padding: 0;
display: flex;
transition: transform 0.55s var(--ease);
will-change: transform;
}
.slide {
flex: 0 0 100%;
min-width: 0;
padding: 4px;
}
/* ---------- Arrows ---------- */
.carousel__arrow {
display: inline-grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: 50%;
border: 1px solid var(--line-strong);
background: var(--surface);
color: var(--ink);
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: transform 0.18s var(--ease), border-color 0.18s var(--ease),
color 0.18s var(--ease), background 0.18s var(--ease);
}
.carousel__arrow svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 2.1;
stroke-linecap: round;
stroke-linejoin: round;
}
.carousel__arrow:hover {
border-color: var(--accent);
color: var(--accent);
transform: translateY(-1px);
}
.carousel__arrow:active {
transform: translateY(0) scale(0.95);
}
/* ---------- Card ---------- */
.card {
position: relative;
margin: 0;
padding: clamp(1.5rem, 4.5vw, 2.6rem);
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.card::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: linear-gradient(90deg, var(--accent), #8b5cf6 55%, #ec4899);
}
.card__rating {
display: flex;
gap: 2px;
margin-bottom: 1rem;
font-size: 1.05rem;
line-height: 1;
}
.star {
color: var(--star);
}
.star--empty {
color: var(--star-empty);
}
.card__mark {
position: absolute;
top: clamp(0.4rem, 2vw, 1rem);
right: clamp(1rem, 4vw, 2rem);
font-family: var(--serif);
font-size: clamp(5rem, 18vw, 9rem);
line-height: 0.8;
color: var(--accent-soft);
pointer-events: none;
user-select: none;
z-index: 0;
}
.card__quote {
position: relative;
z-index: 1;
margin: 0 0 1.6rem;
}
.card__quote p {
margin: 0;
font-family: var(--serif);
font-size: clamp(1.12rem, 3.2vw, 1.45rem);
line-height: 1.5;
letter-spacing: -0.005em;
color: var(--ink);
}
.card__author {
display: flex;
align-items: center;
gap: 0.85rem;
position: relative;
z-index: 1;
padding-top: 1.3rem;
border-top: 1px solid var(--line);
}
.avatar {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 48px;
height: 48px;
border-radius: 50%;
font-size: 0.95rem;
font-weight: 700;
letter-spacing: 0.02em;
color: #fff;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.4);
}
.avatar--a {
background: linear-gradient(135deg, #6366f1, #4338ca);
}
.avatar--b {
background: linear-gradient(135deg, #0ea5e9, #0369a1);
}
.avatar--c {
background: linear-gradient(135deg, #f59e0b, #b45309);
}
.avatar--d {
background: linear-gradient(135deg, #ec4899, #be185d);
}
.card__id {
display: flex;
flex-direction: column;
min-width: 0;
margin-right: auto;
}
.card__name {
font-weight: 700;
font-size: 0.98rem;
letter-spacing: -0.01em;
}
.card__role {
font-size: 0.84rem;
color: var(--ink-faint);
}
.card__logo {
flex: 0 0 auto;
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 11px;
background: var(--accent-soft);
color: var(--accent-ink);
}
.card__logo svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
/* ---------- Controls ---------- */
.carousel__controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1.2rem;
margin-top: clamp(1.1rem, 3vw, 1.6rem);
}
.dots {
display: flex;
align-items: center;
gap: 0.55rem;
}
.dot {
width: 9px;
height: 9px;
padding: 0;
border: none;
border-radius: 999px;
background: var(--line-strong);
cursor: pointer;
transition: width 0.3s var(--ease), background 0.3s var(--ease);
}
.dot:hover {
background: var(--ink-faint);
}
.dot[aria-selected="true"] {
width: 26px;
background: var(--accent);
}
.playpause {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.42rem 0.85rem 0.42rem 0.62rem;
border: 1px solid var(--line-strong);
border-radius: 999px;
background: var(--surface);
color: var(--ink-soft);
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: border-color 0.18s var(--ease), color 0.18s var(--ease);
}
.playpause:hover {
border-color: var(--accent);
color: var(--accent-ink);
}
.playpause__icon {
width: 18px;
height: 18px;
fill: currentColor;
}
.playpause__icon--play {
display: none;
}
.carousel[data-paused="true"] .playpause__icon--pause {
display: none;
}
.carousel[data-paused="true"] .playpause__icon--play {
display: block;
}
/* ---------- Focus visibility ---------- */
:focus-visible {
outline: 3px solid color-mix(in srgb, var(--accent) 55%, transparent);
outline-offset: 2px;
border-radius: 4px;
}
.carousel__arrow:focus-visible,
.playpause:focus-visible {
outline-offset: 3px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translate(-50%, 1.5rem);
padding: 0.7rem 1.1rem;
border-radius: 12px;
background: var(--ink);
color: #fff;
font-size: 0.86rem;
font-weight: 500;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s var(--ease), transform 0.25s var(--ease);
z-index: 30;
}
.toast--show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 560px) {
.carousel {
grid-template-columns: 1fr;
gap: 0;
}
.carousel__viewport {
order: -1;
}
.carousel__arrow {
width: 42px;
height: 42px;
}
.carousel__controls {
margin-top: 1rem;
gap: 0.9rem;
}
/* place arrows beside the controls row on small screens */
.carousel__arrow--prev,
.carousel__arrow--next {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 5;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(4px);
}
.carousel__arrow--prev {
left: 6px;
}
.carousel__arrow--next {
right: 6px;
}
.carousel {
position: relative;
}
}
@media (max-width: 380px) {
.card {
padding: 1.35rem 1.15rem;
}
.card__author {
flex-wrap: wrap;
}
.card__logo {
order: 3;
}
}
@media (prefers-reduced-motion: reduce) {
.carousel__track {
transition: none;
}
.toast {
transition: opacity 0.2s linear;
}
}(function () {
"use strict";
var root = document.querySelector("[data-carousel]");
if (!root) return;
var track = root.querySelector("[data-track]");
var slides = Array.prototype.slice.call(track.querySelectorAll(".slide"));
var prevBtn = root.querySelector("[data-prev]");
var nextBtn = root.querySelector("[data-next]");
var dotsWrap = root.parentElement.querySelector("[data-dots]");
var playBtn = document.querySelector("[data-playpause]");
var playLabel = document.querySelector("[data-playpause-label]");
var toastEl = document.querySelector("[data-toast]");
var count = slides.length;
var index = 0;
var timer = null;
var paused = false;
var INTERVAL = 5500;
var reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
/* ---------- Build dots ---------- */
var dots = [];
slides.forEach(function (_, i) {
var dot = document.createElement("button");
dot.type = "button";
dot.className = "dot";
dot.setAttribute("role", "tab");
dot.setAttribute("aria-label", "Recommendation " + (i + 1) + " of " + count);
dot.addEventListener("click", function () {
goTo(i, true);
});
dotsWrap.appendChild(dot);
dots.push(dot);
});
/* ---------- Toast helper ---------- */
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("toast--show");
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("toast--show");
}, 2200);
}
/* ---------- Render ---------- */
function render() {
track.style.transform = "translateX(" + -index * 100 + "%)";
slides.forEach(function (slide, i) {
var active = i === index;
slide.setAttribute("aria-hidden", active ? "false" : "true");
// keep off-screen slides out of the tab order
var focusables = slide.querySelectorAll("a, button");
focusables.forEach(function (el) {
if (active) el.removeAttribute("tabindex");
else el.setAttribute("tabindex", "-1");
});
});
dots.forEach(function (dot, i) {
dot.setAttribute("aria-selected", i === index ? "true" : "false");
});
}
function goTo(i, userTriggered) {
index = (i + count) % count;
render();
if (userTriggered) restart();
}
function next(userTriggered) {
goTo(index + 1, userTriggered);
}
function prev(userTriggered) {
goTo(index - 1, userTriggered);
}
/* ---------- Autoplay ---------- */
function tick() {
if (!paused) next(false);
}
function start() {
if (reduceMotion) return; // respect reduced motion: no auto-advance
stop();
timer = window.setInterval(tick, INTERVAL);
}
function stop() {
if (timer) {
window.clearInterval(timer);
timer = null;
}
}
function restart() {
if (paused) return;
start();
}
function setPaused(state) {
paused = state;
root.setAttribute("data-paused", state ? "true" : "false");
if (playBtn) playBtn.setAttribute("aria-pressed", state ? "false" : "true");
if (playLabel) playLabel.textContent = state ? "Play" : "Pause";
if (state) stop();
else start();
}
/* ---------- Events ---------- */
nextBtn.addEventListener("click", function () {
next(true);
});
prevBtn.addEventListener("click", function () {
prev(true);
});
if (playBtn) {
playBtn.addEventListener("click", function () {
setPaused(!paused);
toast(paused ? "Autoplay paused" : "Autoplay resumed");
});
}
/* Keyboard nav when focus is inside the carousel */
root.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight") {
e.preventDefault();
next(true);
} else if (e.key === "ArrowLeft") {
e.preventDefault();
prev(true);
} else if (e.key === "Home") {
e.preventDefault();
goTo(0, true);
} else if (e.key === "End") {
e.preventDefault();
goTo(count - 1, true);
}
});
/* Pause on hover / focus, resume after (unless user-paused) */
root.addEventListener("mouseenter", function () {
if (!paused) stop();
});
root.addEventListener("mouseleave", function () {
if (!paused) start();
});
root.addEventListener("focusin", function () {
if (!paused) stop();
});
root.addEventListener("focusout", function () {
if (!root.contains(document.activeElement) && !paused) start();
});
/* Pause when tab is hidden */
document.addEventListener("visibilitychange", function () {
if (document.hidden) stop();
else if (!paused) start();
});
/* ---------- Touch / swipe ---------- */
var startX = 0;
var startY = 0;
var swiping = false;
root.addEventListener(
"touchstart",
function (e) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
swiping = true;
},
{ passive: true }
);
root.addEventListener(
"touchend",
function (e) {
if (!swiping) return;
swiping = false;
var dx = e.changedTouches[0].clientX - startX;
var dy = e.changedTouches[0].clientY - startY;
if (Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy)) {
if (dx < 0) next(true);
else prev(true);
}
},
{ passive: true }
);
/* ---------- Init ---------- */
if (reduceMotion) {
setPaused(true);
} else {
render();
start();
}
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Maya Okafor — Recommendations</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=Inter:wght@400;500;600;700;800&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#testimonials">Skip to recommendations</a>
<main class="wrap">
<section
id="testimonials"
class="recs"
aria-roledescription="carousel"
aria-label="Recommendations for Maya Okafor"
>
<header class="recs__head">
<p class="recs__eyebrow">What people say</p>
<h1 class="recs__title">Recommendations</h1>
<p class="recs__lede">
A few words from the people I’ve designed, shipped and shaped products
with — leads, founders and the engineers in the trenches.
</p>
</header>
<div class="carousel" data-carousel>
<button
class="carousel__arrow carousel__arrow--prev"
type="button"
data-prev
aria-label="Previous recommendation"
>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M15 5l-7 7 7 7" />
</svg>
</button>
<div class="carousel__viewport">
<ul class="carousel__track" data-track aria-live="polite">
<li class="slide" role="group" aria-roledescription="slide" aria-label="1 of 4">
<figure class="card">
<div class="card__rating" role="img" aria-label="Rated 5 out of 5">
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
</div>
<span class="card__mark" aria-hidden="true">“</span>
<blockquote class="card__quote">
<p>
Maya is the rare designer who can hold the whole system in her
head and still sweat the one pixel that matters. She rebuilt
our onboarding from a wireframe to shipped in six weeks and
activation jumped 22%. She made the whole team better.
</p>
</blockquote>
<figcaption class="card__author">
<span class="avatar avatar--a" aria-hidden="true">DR</span>
<span class="card__id">
<span class="card__name">Daniela Rojas</span>
<span class="card__role">VP Product · Northwind Labs</span>
</span>
<span class="card__logo" aria-label="Northwind Labs">
<svg viewBox="0 0 28 28" aria-hidden="true" focusable="false">
<path d="M4 22V6l10 10V6m0 16V6l10 10V6" />
</svg>
</span>
</figcaption>
</figure>
</li>
<li class="slide" role="group" aria-roledescription="slide" aria-label="2 of 4">
<figure class="card">
<div class="card__rating" role="img" aria-label="Rated 5 out of 5">
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
</div>
<span class="card__mark" aria-hidden="true">“</span>
<blockquote class="card__quote">
<p>
Working with Maya felt like adding a second product brain to the
team. She turns fuzzy founder hunches into crisp flows and
prototypes you can actually test on Friday. Our investors kept
asking who designed the deck — that was all her.
</p>
</blockquote>
<figcaption class="card__author">
<span class="avatar avatar--b" aria-hidden="true">TO</span>
<span class="card__id">
<span class="card__name">Tobias Øvergaard</span>
<span class="card__role">Founder & CEO · Tideglass</span>
</span>
<span class="card__logo" aria-label="Tideglass">
<svg viewBox="0 0 28 28" aria-hidden="true" focusable="false">
<circle cx="14" cy="14" r="9" />
<path d="M5 17c4 3 14 3 18 0" />
</svg>
</span>
</figcaption>
</figure>
</li>
<li class="slide" role="group" aria-roledescription="slide" aria-label="3 of 4">
<figure class="card">
<div class="card__rating" role="img" aria-label="Rated 4 out of 5">
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star star--empty" aria-hidden="true">★</span>
</div>
<span class="card__mark" aria-hidden="true">“</span>
<blockquote class="card__quote">
<p>
As the engineer who built half her designs, I can say Maya’s
specs are a joy — tokens named like a developer would, edge
cases drawn out, motion timed in milliseconds. Handoff stopped
being a fight and started being a conversation.
</p>
</blockquote>
<figcaption class="card__author">
<span class="avatar avatar--c" aria-hidden="true">PK</span>
<span class="card__id">
<span class="card__name">Priya Kannan</span>
<span class="card__role">Staff Engineer · Northwind Labs</span>
</span>
<span class="card__logo" aria-label="Northwind Labs">
<svg viewBox="0 0 28 28" aria-hidden="true" focusable="false">
<path d="M4 22V6l10 10V6m0 16V6l10 10V6" />
</svg>
</span>
</figcaption>
</figure>
</li>
<li class="slide" role="group" aria-roledescription="slide" aria-label="4 of 4">
<figure class="card">
<div class="card__rating" role="img" aria-label="Rated 5 out of 5">
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
<span class="star" aria-hidden="true">★</span>
</div>
<span class="card__mark" aria-hidden="true">“</span>
<blockquote class="card__quote">
<p>
Maya mentored two of our juniors through their first real design
system. She’s generous with the why, ruthless about the what,
and somehow makes critique feel like a gift. The team’s craft
bar is permanently higher because she was here.
</p>
</blockquote>
<figcaption class="card__author">
<span class="avatar avatar--d" aria-hidden="true">SH</span>
<span class="card__id">
<span class="card__name">Sam Hollings</span>
<span class="card__role">Design Director · Atelier Form</span>
</span>
<span class="card__logo" aria-label="Atelier Form">
<svg viewBox="0 0 28 28" aria-hidden="true" focusable="false">
<rect x="5" y="5" width="18" height="18" rx="4" />
<path d="M5 14h18M14 5v18" />
</svg>
</span>
</figcaption>
</figure>
</li>
</ul>
</div>
<button
class="carousel__arrow carousel__arrow--next"
type="button"
data-next
aria-label="Next recommendation"
>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
<div class="carousel__controls">
<div class="dots" role="tablist" aria-label="Choose recommendation" data-dots></div>
<button class="playpause" type="button" data-playpause aria-pressed="true">
<svg class="playpause__icon playpause__icon--pause" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<rect x="7" y="6" width="3.4" height="12" rx="1" />
<rect x="13.6" y="6" width="3.4" height="12" rx="1" />
</svg>
<svg class="playpause__icon playpause__icon--play" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M8 5.5v13l11-6.5z" />
</svg>
<span class="playpause__label" data-playpause-label>Pause</span>
</button>
</div>
</section>
</main>
<div class="toast" data-toast role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Testimonial / Recommendation Block
A drop-in recommendations section for a single-person portfolio. A featured quote card pairs a large serif pull-quote with a star rating, a CSS-gradient avatar showing the recommender’s initials, their name, role and company, and a small inline-SVG company logo. Four credible recommendations — from a VP of Product, a founder, a staff engineer and a design director — rotate through the same elegant card.
The carousel is built in vanilla JS and every control actually works. It auto-advances on a gentle timer that you can pause or resume from a labelled play/pause toggle, and it also pauses on hover, on keyboard focus and when the browser tab is hidden, so it never moves out from under a reader. Navigate with the prev/next arrows, the active-state dots, the arrow / Home / End keys, or a left-right swipe on touch. Slides move with a smooth CSS transform and off-screen cards are pulled out of the tab order, while an aria-live region announces changes.
The neutral base uses Inter for UI and Newsreader for the quotes, keeps WCAG AA contrast, and ships visible :focus-visible rings plus a skip link. It honours prefers-reduced-motion by disabling autoplay, and the layout collapses gracefully to a single column with overlaid arrows down to about 360px.
Illustrative portfolio — fictional person and projects.