UI Component
✨ NewMarketing
Infinite Marquee Testimonial Wall
A warm, editorial testimonial section with three rows of review cards scrolling in opposite directions, pause-on-hover, star ratings, highlighted key phrases, generated avatars and animated count-up stats. Testimonials are rendered from a simple JavaScript array, so adding reviews is easy.
September 27, 20261 views
- Testimonial
- testimonial
- marquee
- reviews
- social-proof
- landing-page
- infinite-scroll
Features Included
Three marquee rows moving in alternating directions at different speeds
Pause on hover so visitors can read a card
Star ratings, source badge and highlighted key phrases
Initial-based gradient avatars (no images needed)
Count-up stats that animate when scrolled into view
Edit one JavaScript array to change all testimonials
Infinite Marquee Testimonial Wall
A 'wall of love' section that builds instant trust — three infinite rows of reviews, count-up stats and a clean editorial look. HTML, CSS and vanilla JavaScript.
What's Included
- Three marquee rows moving in alternating directions at different speeds
- Pause on hover so visitors can read a card
- Star ratings, source badge and highlighted key phrases
- Initial-based gradient avatars (no images needed)
- Count-up stats that animate when scrolled into view
- Edit one JavaScript array to change all testimonials
Dependencies
- Google Fonts — DM Sans + Instrument Serif https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap
Already linked inside the component's files — no extra setup needed.
Desktop Preview
HTML
html1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>Infinite Testimonial Wall | MetaSkillGroup</title> 7 <link rel="stylesheet" href="style.css" /> 8 </head> 9 <body> 10 <!-- created by metaskillgroup-team --> 11 <section class="testimonials"> 12 <header class="t-head"> 13 <span class="eyebrow">Wall of love</span> 14 <h1>Don't take our word for it.<br /><em>Take theirs.</em></h1> 15 <div class="t-stats"> 16 <div><strong data-count="12000">0</strong><span>happy customers</span></div> 17 <div><strong data-count="4.9" data-decimals="1">0</strong><span>average rating</span></div> 18 <div><strong data-count="98" data-suffix="%">0</strong><span>would recommend</span></div> 19 </div> 20 </header> 21 22 <!-- Rows are filled from script.js --> 23 <div class="wall"> 24 <div class="row" data-direction="left"><div class="track"></div></div> 25 <div class="row" data-direction="right"><div class="track"></div></div> 26 <div class="row" data-direction="left"><div class="track"></div></div> 27 </div> 28 29 <div class="t-cta"> 30 <a href="#" class="btn">Read all 2,300 reviews</a> 31 <a href="#" class="btn btn-outline">Share your story</a> 32 </div> 33 </section> 34 35 <script src="script.js"></script> 36 </body> 37</html> 38
CSS
css1/* Importing Google Fonts - DM Sans + Instrument Serif */ 2@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap"); 3 4* { 5 margin: 0; 6 padding: 0; 7 box-sizing: border-box; 8 font-family: "DM Sans", sans-serif; 9} 10 11:root { 12 --bg: #faf6ef; 13 --card: #ffffff; 14 --text: #1c1917; 15 --muted: #78716c; 16 --border: #ebe4d8; 17 --accent: #ea580c; 18 --star: #f59e0b; 19} 20 21body { 22 background: var(--bg); 23 color: var(--text); 24 overflow-x: hidden; 25} 26 27a { 28 color: inherit; 29 text-decoration: none; 30} 31 32.testimonials { 33 padding: 90px 0 100px; 34} 35 36/* ---------- Heading ---------- */ 37.t-head { 38 max-width: 820px; 39 margin: 0 auto 56px; 40 padding: 0 20px; 41 text-align: center; 42} 43 44.eyebrow { 45 display: inline-flex; 46 align-items: center; 47 gap: 8px; 48 font-size: 0.82rem; 49 font-weight: 700; 50 letter-spacing: 0.12em; 51 text-transform: uppercase; 52 color: var(--accent); 53} 54 55.eyebrow::before, 56.eyebrow::after { 57 content: ""; 58 width: 24px; 59 height: 1px; 60 background: currentColor; 61} 62 63.t-head h1 { 64 margin: 18px 0 36px; 65 font-family: "Instrument Serif", serif; 66 font-weight: 400; 67 font-size: clamp(2.4rem, 6vw, 4.2rem); 68 line-height: 1.05; 69 letter-spacing: -0.01em; 70} 71 72.t-head h1 em { 73 color: var(--accent); 74} 75 76.t-stats { 77 display: flex; 78 justify-content: center; 79 gap: clamp(20px, 6vw, 64px); 80 flex-wrap: wrap; 81} 82 83.t-stats div { 84 display: flex; 85 flex-direction: column; 86 align-items: center; 87} 88 89.t-stats strong { 90 font-family: "Instrument Serif", serif; 91 font-size: 2.6rem; 92 font-weight: 400; 93 line-height: 1; 94 font-variant-numeric: tabular-nums; 95} 96 97.t-stats span { 98 margin-top: 6px; 99 font-size: 0.85rem; 100 color: var(--muted); 101} 102 103/* ---------- Marquee wall ---------- */ 104.wall { 105 display: flex; 106 flex-direction: column; 107 gap: 20px; 108 /* Fade the left and right edges */ 109 mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); 110 -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); 111} 112 113.row { 114 overflow: hidden; 115} 116 117.track { 118 display: flex; 119 gap: 20px; 120 width: max-content; 121 padding: 6px 0; 122 animation: marquee var(--duration, 50s) linear infinite; 123} 124 125.row[data-direction="right"] .track { 126 animation-direction: reverse; 127} 128 129/* Pause a row while the user reads a card */ 130.row:hover .track { 131 animation-play-state: paused; 132} 133 134@keyframes marquee { 135 to { 136 transform: translateX(calc(-50% - 10px)); 137 } 138} 139 140/* ---------- Card ---------- */ 141.t-card { 142 position: relative; 143 width: 360px; 144 flex-shrink: 0; 145 padding: 24px; 146 border-radius: 20px; 147 background: var(--card); 148 border: 1px solid var(--border); 149 box-shadow: 0 1px 2px rgba(28, 25, 23, 0.04); 150 transition: 151 transform 0.3s ease, 152 box-shadow 0.3s ease, 153 border-color 0.3s ease; 154} 155 156.t-card:hover { 157 transform: translateY(-4px) rotate(-0.6deg); 158 border-color: #f3c9a8; 159 box-shadow: 0 20px 40px rgba(234, 88, 12, 0.12); 160} 161 162.t-card::after { 163 content: "\201C"; 164 position: absolute; 165 top: 6px; 166 right: 20px; 167 font-family: "Instrument Serif", serif; 168 font-size: 5rem; 169 line-height: 1; 170 color: #f5e6d6; 171 pointer-events: none; 172} 173 174.t-stars { 175 color: var(--star); 176 letter-spacing: 2px; 177 font-size: 0.95rem; 178} 179 180.t-stars .dim { 181 color: #e7e0d4; 182} 183 184.t-text { 185 position: relative; 186 z-index: 1; 187 margin: 12px 0 20px; 188 font-size: 0.98rem; 189 line-height: 1.6; 190} 191 192.t-text mark { 193 background: linear-gradient(transparent 60%, #fed7aa 60%); 194 color: inherit; 195} 196 197.t-user { 198 display: flex; 199 align-items: center; 200 gap: 12px; 201} 202 203.t-avatar { 204 display: grid; 205 place-items: center; 206 width: 42px; 207 height: 42px; 208 border-radius: 50%; 209 font-size: 0.85rem; 210 font-weight: 700; 211 color: #fff; 212 background: linear-gradient(135deg, hsl(var(--h) 75% 60%), hsl(calc(var(--h) + 40) 75% 50%)); 213} 214 215.t-user strong { 216 display: block; 217 font-size: 0.93rem; 218} 219 220.t-user small { 221 font-size: 0.8rem; 222 color: var(--muted); 223} 224 225.t-source { 226 margin-left: auto; 227 padding: 4px 10px; 228 border-radius: 99px; 229 font-size: 0.72rem; 230 font-weight: 600; 231 color: var(--muted); 232 background: var(--bg); 233} 234 235/* ---------- CTA ---------- */ 236.t-cta { 237 display: flex; 238 justify-content: center; 239 flex-wrap: wrap; 240 gap: 12px; 241 margin-top: 56px; 242 padding: 0 20px; 243} 244 245.btn { 246 padding: 14px 26px; 247 border-radius: 99px; 248 font-weight: 600; 249 color: #fff; 250 background: var(--text); 251 transition: 252 transform 0.2s ease, 253 background 0.2s ease; 254} 255 256.btn:hover { 257 transform: translateY(-2px); 258 background: var(--accent); 259} 260 261.btn-outline { 262 color: var(--text); 263 background: transparent; 264 border: 1.5px solid var(--text); 265} 266 267.btn-outline:hover { 268 color: #fff; 269 border-color: var(--accent); 270} 271 272/* ---------- Responsive ---------- */ 273@media (max-width: 640px) { 274 .t-card { 275 width: 290px; 276 padding: 20px; 277 } 278 279 .wall { 280 gap: 14px; 281 } 282 283 .track { 284 gap: 14px; 285 } 286} 287 288@media (prefers-reduced-motion: reduce) { 289 .track { 290 animation: none; 291 } 292 293 .row { 294 overflow-x: auto; 295 } 296} 297
JavaScript
javascript1// Edit this list to change the testimonials 2const testimonials = [ 3 { 4 name: "Aarav Mehta", 5 role: "CTO, Finlytics", 6 rating: 5, 7 source: "G2", 8 text: "We moved our entire team over in a weekend. <mark>Onboarding took less than an hour</mark> and nobody has looked back.", 9 }, 10 { 11 name: "Sophie Laurent", 12 role: "Product Designer", 13 rating: 5, 14 source: "Twitter", 15 text: "Finally a tool that feels as good as the things we design with it. <mark>The attention to detail is unreal.</mark>", 16 }, 17 { 18 name: "Marcus Chen", 19 role: "Founder, Shipfast", 20 rating: 5, 21 source: "Product Hunt", 22 text: "It replaced four different subscriptions for us. <mark>We save around $900 every month.</mark>", 23 }, 24 { 25 name: "Priya Nair", 26 role: "Engineering Manager", 27 rating: 4, 28 source: "Capterra", 29 text: "Sprint planning used to take half a day. Now it's a 20-minute call and <mark>everyone knows what to do</mark>.", 30 }, 31 { 32 name: "Lucas Oliveira", 33 role: "Freelance Developer", 34 rating: 5, 35 source: "Twitter", 36 text: "The API is clean, the docs are great and support replied in <mark>under five minutes</mark>. Rare these days.", 37 }, 38 { 39 name: "Hannah Kim", 40 role: "Head of Marketing", 41 rating: 5, 42 source: "G2", 43 text: "Our campaign launches are <mark>twice as fast</mark> since we started using the shared boards.", 44 }, 45 { 46 name: "Omar Haddad", 47 role: "Startup Advisor", 48 rating: 5, 49 source: "LinkedIn", 50 text: "I recommend it to every founder I work with. It just <mark>gets out of your way</mark> and lets you ship.", 51 }, 52 { 53 name: "Emily Watson", 54 role: "Ops Lead, Brightside", 55 rating: 4, 56 source: "Capterra", 57 text: "Automations alone saved us <mark>12 hours a week</mark>. The reporting is the cherry on top.", 58 }, 59 { 60 name: "Rohan Gupta", 61 role: "Full-stack Engineer", 62 rating: 5, 63 source: "Product Hunt", 64 text: "Dark mode, keyboard shortcuts, instant search. <mark>Built by people who actually use it.</mark>", 65 }, 66 { 67 name: "Isabella Rossi", 68 role: "Creative Director", 69 rating: 5, 70 source: "LinkedIn", 71 text: "Clients love the shareable previews. <mark>Approvals that took days now take minutes.</mark>", 72 }, 73 { 74 name: "Daniel Okafor", 75 role: "CEO, Paystack Labs", 76 rating: 5, 77 source: "G2", 78 text: "Scaled from 5 to 120 people without changing tools once. <mark>It grows with you.</mark>", 79 }, 80 { 81 name: "Mia Johansson", 82 role: "UX Researcher", 83 rating: 5, 84 source: "Twitter", 85 text: "I've tried everything out there. <mark>This is the first one that stuck</mark> for more than a month.", 86 }, 87]; 88 89const rows = document.querySelectorAll(".row"); 90 91// Build the HTML for one testimonial card 92const cardHTML = (t) => { 93 const initials = t.name 94 .split(" ") 95 .map((n) => n[0]) 96 .join(""); 97 // Stable hue per person so avatar colours don't change on reload 98 const hue = [...t.name].reduce((sum, ch) => sum + ch.charCodeAt(0), 0) % 360; 99 const stars = 100 "★".repeat(t.rating) + 101 `<span class="dim">${"★".repeat(5 - t.rating)}</span>`; 102 103 return ` 104 <figure class="t-card"> 105 <div class="t-stars" aria-label="${t.rating} out of 5 stars">${stars}</div> 106 <blockquote class="t-text">${t.text}</blockquote> 107 <figcaption class="t-user"> 108 <span class="t-avatar" style="--h: ${hue}">${initials}</span> 109 <span><strong>${t.name}</strong><small>${t.role}</small></span> 110 <span class="t-source">${t.source}</span> 111 </figcaption> 112 </figure>`; 113}; 114 115// Split testimonials across rows, each row starting at a different offset 116rows.forEach((row, i) => { 117 const track = row.querySelector(".track"); 118 const offset = i * 4; 119 const list = [ 120 ...testimonials.slice(offset), 121 ...testimonials.slice(0, offset), 122 ]; 123 const html = list.map(cardHTML).join(""); 124 125 // Content twice so translateX(-50%) loops seamlessly 126 track.innerHTML = html + html; 127 track.style.setProperty("--duration", `${60 + i * 12}s`); 128 [...track.children] 129 .slice(list.length) 130 .forEach((el) => el.setAttribute("aria-hidden", "true")); 131}); 132 133// Count-up stats when they scroll into view 134const counters = document.querySelectorAll("[data-count]"); 135 136const runCounter = (el) => { 137 const target = Number(el.dataset.count); 138 const decimals = Number(el.dataset.decimals || 0); 139 const suffix = el.dataset.suffix || ""; 140 const duration = 1600; 141 const start = performance.now(); 142 143 const step = (now) => { 144 const p = Math.min((now - start) / duration, 1); 145 const value = target * (1 - Math.pow(1 - p, 4)); 146 el.textContent = 147 (decimals 148 ? value.toFixed(decimals) 149 : Math.round(value).toLocaleString()) + 150 (p === 1 && target >= 1000 ? "+" : "") + 151 suffix; 152 if (p < 1) requestAnimationFrame(step); 153 }; 154 requestAnimationFrame(step); 155}; 156 157const observer = new IntersectionObserver( 158 (entries) => { 159 entries.forEach((entry) => { 160 if (entry.isIntersecting) { 161 runCounter(entry.target); 162 observer.unobserve(entry.target); 163 } 164 }); 165 }, 166 { threshold: 0.6 }, 167); 168 169counters.forEach((el) => observer.observe(el)); 170
Built by Meta Skill Group — copy it, use it, make it yours.
Built With
🌐
HTML5
🎨
CSS3
⚡
JavaScript
Compatibility
Browsers
ChromeOperaBraveFirefoxSafariArcVivaldiEdge
Devices
DesktopSmartwatchTVTabletMobile
Frameworks
Vanilla JSHTML/CSS
Found this useful?

Written by
MetaSkillGroup
September 27, 2026