Logo Grid
Client logos in a hairline grid, grey and all the same weight. A cell can link to its case study: on hover or focus the logo rolls up and the result rises in. Tiers for sponsors.
View codeA testimonials section: three columns of cards drift slowly in opposite directions, with no end and no jump, faded top and bottom. A column under the mouse slows to a stop; a pause button stops the wall.
With your AI
Say it to your AI. It gets the exact code below, not its best guess, and knows where it goes.
Add the Kodza Testimonial Wall to my page.
claude mcp add -t http kodza https://kodza.dev/api/mcp
<section class="kodza-testimonial-wall" data-kodza-testimonial-wall>
<div class="kodza-testimonial-wall__inner">
<div class="kodza-testimonial-wall__head">
<div class="kodza-testimonial-wall__heading">
<p class="kodza-testimonial-wall__eyebrow">Customers</p>
<h2 class="kodza-testimonial-wall__title">Sent. Paid. Forgotten.</h2>
</div>
<button class="kodza-testimonial-wall__pause" type="button" aria-pressed="false" aria-label="Pause the testimonials"><svg class="kodza-testimonial-wall__ring" viewBox="0 0 40 40" aria-hidden="true"><circle cx="20" cy="20" r="19.25" /></svg><svg class="kodza-testimonial-wall__icon" viewBox="0 0 16 16" aria-hidden="true"><path class="kodza-testimonial-wall__bars" d="M5.5 4v8M10.5 4v8" /><path class="kodza-testimonial-wall__play" d="M5.5 3.8v8.4L12 8z" /></svg></button>
</div>
<div class="kodza-testimonial-wall__wall">
<div class="kodza-testimonial-wall__cards">
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>I send invoices from my phone between two fittings. Most clients have paid before I'm back at the counter.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">CL</span><span class="kodza-testimonial-wall__name">Claire Lebrun</span><span class="kodza-testimonial-wall__role">Owner, Atelier Lebrun · Le Havre</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>The API docs open on a curl that works. We took our first payment the same afternoon.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">TF</span><span class="kodza-testimonial-wall__name">Tomás Ferreira</span><span class="kodza-testimonial-wall__role">Developer, Ondula · Porto</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>We moved 4,000 customers off our old billing in one weekend. Nobody noticed, which was the whole point.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">JW</span><span class="kodza-testimonial-wall__name">Jonas Weber</span><span class="kodza-testimonial-wall__role">CTO, Kollektiv Nord · Berlin</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>Reminders go out on their own, politely. I used to write those at midnight, less politely.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">AB</span><span class="kodza-testimonial-wall__name">Aïcha Benali</span><span class="kodza-testimonial-wall__role">Illustrator · Montréal</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>My accountant asked what changed. The exports: clean CSV, the right columns, nothing to retype.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">CD</span><span class="kodza-testimonial-wall__name">Camille Dubreuil</span><span class="kodza-testimonial-wall__role">Manager, Maison Ternes · Nantes</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>No sales call to get an API key. I signed up during stand-up and it was live before stand-up ended.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">LH</span><span class="kodza-testimonial-wall__name">Lucas Hartmann</span><span class="kodza-testimonial-wall__role">Lead developer, Studio Rapp · Ghent</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>VAT in four countries, done right, and no more spreadsheet called FINAL_v7.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">SM</span><span class="kodza-testimonial-wall__name">Sofia Marques</span><span class="kodza-testimonial-wall__role">Finance, Lume Ceramics · Lisbon</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>It does three things and does them properly. I've paid for tools that did forty things badly.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">PV</span><span class="kodza-testimonial-wall__name">Pieter de Vries</span><span class="kodza-testimonial-wall__role">Founder, Kade Studio · Rotterdam</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>Support answered with a fix and a link to the changelog. Not a ticket number.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">HL</span><span class="kodza-testimonial-wall__name">Hugo Lemaire</span><span class="kodza-testimonial-wall__role">Freelance developer · Marseille</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>We switched the bakery's card reader and invoices on a Monday. By Friday I'd stopped thinking about it.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">AR</span><span class="kodza-testimonial-wall__name">Ana Ruiz</span><span class="kodza-testimonial-wall__role">Owner, Forn Ruiz · Bilbao</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>Webhooks retry, sign their payload and show you the response. Basic, apparently rare.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">MC</span><span class="kodza-testimonial-wall__name">Matteo Conti</span><span class="kodza-testimonial-wall__role">Back-end developer, Officina 14 · Turin</span></figcaption>
</figure>
<figure class="kodza-testimonial-wall__card">
<blockquote class="kodza-testimonial-wall__quote"><p>Payouts land on day two, every time. Cash flow stopped being a weekly guessing game.</p></blockquote>
<figcaption class="kodza-testimonial-wall__who"><span class="kodza-testimonial-wall__face" aria-hidden="true">FL</span><span class="kodza-testimonial-wall__name">Freya Lindqvist</span><span class="kodza-testimonial-wall__role">Co-founder, Halvø Coffee · Copenhagen</span></figcaption>
</figure>
</div>
</div>
</div>
</section>
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Testimonial Wall
A testimonials section: cards in three columns that drift slowly, one up, the next down, with
no end and no jump, faded at the top and bottom. Point at a column and it eases to a stop while
the card under the pointer firms its border; the pause button in the header stops the whole wall
(so do the focus inside, a wall out of view and a hidden tab). Two columns on a tablet, one on a
phone, and a still wall in reduced motion.
Everything is tuned with the --kz-testimonial-wall-* variables. */
.kodza-testimonial-wall {
--kz-testimonial-wall-ink: #0a0a0a;
--kz-testimonial-wall-dim: rgba(10, 10, 10, 0.64);
--kz-testimonial-wall-card: #ffffff;
--kz-testimonial-wall-line: rgba(10, 10, 10, 0.1);
--kz-testimonial-wall-line-strong: rgba(10, 10, 10, 0.42); /* the pointed card */
--kz-testimonial-wall-face: #e6e6e3;
--kz-testimonial-wall-track: rgba(10, 10, 10, 0.16);
--kz-testimonial-wall-bg: transparent;
--kz-testimonial-wall-columns: 3; /* 2 under 860px, 1 under 560px (below) */
--kz-testimonial-wall-height: 640px; /* the window the columns drift through */
--kz-testimonial-wall-speed: 24; /* pixels per second */
--kz-testimonial-wall-fade: 112px;
--kz-testimonial-wall-gap: 16px;
--kz-testimonial-wall-width: 1200px;
--kz-testimonial-wall-pad-y: clamp(64px, 8vw, 112px);
--kz-testimonial-wall-gutter: clamp(16px, 4vw, 40px);
--kz-testimonial-wall-radius: calc(16px * var(--kz-radius-scale, 1));
--kz-testimonial-wall-mono: var(--kz-font-mono, "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace);
--kz-testimonial-wall-ease: cubic-bezier(0.22, 1, 0.36, 1);
container: kodza-testimonial-wall / inline-size;
box-sizing: border-box;
width: 100%;
padding-block: var(--kz-testimonial-wall-pad-y);
padding-left: max(var(--kz-testimonial-wall-gutter), env(safe-area-inset-left, 0px));
padding-right: max(var(--kz-testimonial-wall-gutter), env(safe-area-inset-right, 0px));
background: var(--kz-testimonial-wall-bg);
color: var(--kz-testimonial-wall-ink);
}
.kodza-testimonial-wall__inner {
max-width: var(--kz-testimonial-wall-width);
margin: 0 auto;
}
/* The header: label and title on the left, the pause button at the end of the title's line */
.kodza-testimonial-wall__head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: clamp(28px, 3.6vw, 48px);
}
.kodza-testimonial-wall__heading {
min-width: 0;
}
.kodza-testimonial-wall__eyebrow {
margin: 0 0 18px;
font-family: var(--kz-font-label, var(--kz-testimonial-wall-mono));
font-size: var(--kz-font-label-size, 12.5px);
font-weight: var(--kz-font-label-weight, 500);
line-height: var(--kz-font-label-leading, inherit);
letter-spacing: var(--kz-font-label-tracking, 0.02em);
text-transform: var(--kz-font-label-case, inherit);
color: var(--kz-testimonial-wall-dim);
}
.kodza-testimonial-wall__title {
margin: 0;
font-family: var(--kz-font-display, inherit);
font-size: calc(clamp(32px, 4.6vw, 56px) * var(--kz-font-display-scale, 1));
font-weight: var(--kz-font-display-weight, 600);
line-height: 1.02;
letter-spacing: var(--kz-font-display-tracking, -0.045em);
text-transform: var(--kz-font-display-case, none);
text-wrap: balance;
color: inherit;
}
/* The pause button: a ring, two bars that turn into a play arrow once pressed */
.kodza-testimonial-wall__pause {
position: relative;
flex: none;
display: grid;
place-items: center;
width: 40px;
height: 40px;
margin: 0;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--kz-testimonial-wall-ink);
cursor: pointer;
}
/* Nothing to pause until the wall moves */
.kodza-testimonial-wall:not(.is-live) .kodza-testimonial-wall__pause {
visibility: hidden;
}
.kodza-testimonial-wall__pause:focus-visible {
outline: 2px solid var(--kz-testimonial-wall-ink);
outline-offset: 3px;
}
.kodza-testimonial-wall__ring,
.kodza-testimonial-wall__icon {
grid-area: 1 / 1;
}
.kodza-testimonial-wall__ring {
width: 40px;
height: 40px;
fill: none;
stroke: var(--kz-testimonial-wall-track);
stroke-width: 1.5;
transition: stroke calc(0.2s * var(--kz-motion-scale, 1));
}
@media (hover: hover) {
.kodza-testimonial-wall__pause:hover .kodza-testimonial-wall__ring {
stroke: var(--kz-testimonial-wall-ink);
}
}
.kodza-testimonial-wall__icon {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.kodza-testimonial-wall__play {
fill: currentColor;
stroke-width: 1.2;
opacity: 0;
transform: scale(0.6);
transform-origin: 50% 50%;
transition:
opacity calc(0.2s * var(--kz-motion-scale, 1)),
transform calc(0.25s * var(--kz-motion-scale, 1)) var(--kz-testimonial-wall-ease);
}
.kodza-testimonial-wall__bars {
transform-origin: 50% 50%;
transition:
opacity calc(0.2s * var(--kz-motion-scale, 1)),
transform calc(0.25s * var(--kz-motion-scale, 1)) var(--kz-testimonial-wall-ease);
}
.kodza-testimonial-wall__pause[aria-pressed="true"] .kodza-testimonial-wall__bars {
opacity: 0;
transform: scale(0.6);
}
.kodza-testimonial-wall__pause[aria-pressed="true"] .kodza-testimonial-wall__play {
opacity: 1;
transform: none;
}
/* The window: a fixed height, faded top and bottom (so the loop's seam is never seen) */
.kodza-testimonial-wall__wall {
height: var(--kz-testimonial-wall-height);
overflow: hidden;
-webkit-mask-image: linear-gradient(to bottom, transparent, #000 var(--kz-testimonial-wall-fade), #000 calc(100% - var(--kz-testimonial-wall-fade)), transparent);
mask-image: linear-gradient(to bottom, transparent, #000 var(--kz-testimonial-wall-fade), #000 calc(100% - var(--kz-testimonial-wall-fade)), transparent);
}
/* Before the script, a plain grid; then one column element per column, dealt by the script */
.kodza-testimonial-wall__cards {
display: grid;
grid-template-columns: repeat(var(--kz-testimonial-wall-columns), minmax(0, 1fr));
align-items: start;
gap: var(--kz-testimonial-wall-gap);
}
.kodza-testimonial-wall__column {
min-width: 0;
}
.kodza-testimonial-wall__track {
display: flex;
flex-direction: column;
gap: var(--kz-testimonial-wall-gap);
}
.kodza-testimonial-wall__card {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 22px;
margin: 0;
padding: 28px;
border: 1px solid var(--kz-testimonial-wall-line);
border-radius: var(--kz-testimonial-wall-radius);
background: var(--kz-testimonial-wall-card);
transition: border-color calc(0.2s * var(--kz-motion-scale, 1));
}
/* The pointed card firms its border while its column slows to a stop */
@media (hover: hover) {
.kodza-testimonial-wall__card:hover {
border-color: var(--kz-testimonial-wall-line-strong);
}
}
.kodza-testimonial-wall__quote {
margin: 0;
padding: 0;
border: 0;
font-size: 15px;
line-height: 1.55;
text-wrap: pretty;
}
.kodza-testimonial-wall__quote p {
margin: 0;
}
.kodza-testimonial-wall__quote p + p {
margin-top: 0.8em;
}
/* The person: the face on two lines, the name over the role and company */
.kodza-testimonial-wall__who {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
grid-template-rows: auto auto;
align-items: center;
column-gap: 12px;
row-gap: 3px;
}
.kodza-testimonial-wall__face {
grid-row: 1 / 3;
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: var(--kz-radius-pill, 999px);
background: var(--kz-testimonial-wall-face);
object-fit: cover;
font-size: 13px;
font-weight: 600;
line-height: 1;
}
.kodza-testimonial-wall__name {
align-self: end;
font-size: 14px;
font-weight: 500;
line-height: 1.3;
}
.kodza-testimonial-wall__role {
align-self: start;
font-family: var(--kz-font-label, var(--kz-testimonial-wall-mono));
font-size: var(--kz-font-label-size, 12px);
font-weight: var(--kz-font-label-weight, 400);
line-height: var(--kz-font-label-leading, 1.4);
letter-spacing: var(--kz-font-label-tracking, 0);
text-transform: var(--kz-font-label-case, inherit);
color: var(--kz-testimonial-wall-dim);
}
/* A still wall (reduced motion, or a site pace of 0): every card in full, nothing faded */
.kodza-testimonial-wall.is-still .kodza-testimonial-wall__wall {
height: auto;
-webkit-mask-image: none;
mask-image: none;
}
/* Two columns on a tablet, one on a phone, with a shorter window */
@container kodza-testimonial-wall (max-width: 860px) {
.kodza-testimonial-wall__wall {
--kz-testimonial-wall-columns: 2;
}
}
@container kodza-testimonial-wall (max-width: 560px) {
.kodza-testimonial-wall__wall {
--kz-testimonial-wall-columns: 1;
--kz-testimonial-wall-height: 560px;
--kz-testimonial-wall-fade: 88px;
}
.kodza-testimonial-wall__card {
padding: 24px;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-testimonial-wall .kodza-testimonial-wall__wall {
height: auto;
-webkit-mask-image: none;
mask-image: none;
}
.kodza-testimonial-wall *,
.kodza-testimonial-wall *::before,
.kodza-testimonial-wall *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
/* kodza-guard 879f5e34b5c0 — generated by Kodza, do not edit.
Theme guard: keeps a site's own CSS (WordPress themes, Bootstrap, CSS resets…) off this
component. :not(#_) is always true and weighs as much as an id, so these lines win over
theme rules made of classes and tags. Customise the component through its --kz-* variables. */
/* What the browser gives, where the component sets it only in some states: at the lowest weight, so its own rules below put it back */
:where(.kodza-testimonial-wall__eyebrow):not(#_),
:where(.kodza-testimonial-wall__title):not(#_),
:where(.kodza-testimonial-wall__icon):not(#_),
:where(.kodza-testimonial-wall__quote):not(#_),
:where(.kodza-testimonial-wall__who):not(#_),
:where(.kodza-testimonial-wall__quote p:where(:is(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))):not(.kodza-testimonial-wall__quote :not(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))) *))):not(#_) {
transition-duration: revert;
}
:where(.kodza-testimonial-wall__pause):not(#_) {
outline: revert;
outline-offset: revert;
transition-duration: revert;
}
/* What the browser gives, on everything the component leaves to it */
.kodza-testimonial-wall__inner:not(#_),
.kodza-testimonial-wall__head:not(#_),
.kodza-testimonial-wall__heading:not(#_),
.kodza-testimonial-wall__bars:not(#_),
.kodza-testimonial-wall__play:not(#_),
.kodza-testimonial-wall__wall:not(#_),
.kodza-testimonial-wall__cards:not(#_),
.kodza-testimonial-wall__column:not(#_),
.kodza-testimonial-wall__track:not(#_),
.kodza-testimonial-wall__face:not(#_),
.kodza-testimonial-wall__name:not(#_),
.kodza-testimonial-wall__role:not(#_) {
box-sizing: revert;
}
.kodza-testimonial-wall__eyebrow:not(#_),
.kodza-testimonial-wall__title:not(#_) {
border: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
font-style: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
transition-property: revert;
transition-timing-function: revert;
transition-delay: revert;
}
.kodza-testimonial-wall__pause:not(#_) {
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
box-shadow: revert;
appearance: revert;
max-width: revert;
max-height: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
transition-property: revert;
transition-timing-function: revert;
transition-delay: revert;
white-space: revert;
min-height: revert;
}
.kodza-testimonial-wall__ring:not(#_) {
border: revert;
margin: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-testimonial-wall__icon:not(#_),
.kodza-testimonial-wall__who:not(#_) {
border: revert;
margin: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
transition-property: revert;
transition-timing-function: revert;
transition-delay: revert;
}
.kodza-testimonial-wall__card:not(#_) {
outline: revert;
text-decoration: revert;
list-style: revert;
float: revert;
position: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
}
.kodza-testimonial-wall__quote:not(#_) {
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-weight: revert;
font-style: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
transition-property: revert;
transition-timing-function: revert;
transition-delay: revert;
}
.kodza-testimonial-wall__quote p:where(:is(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))):not(.kodza-testimonial-wall__quote :not(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))) *)):not(#_) {
border: revert;
padding: revert;
border-radius: revert;
outline: revert;
text-decoration: revert;
list-style: revert;
box-sizing: revert;
float: revert;
position: revert;
background-color: revert;
background-image: revert;
box-shadow: revert;
outline-offset: revert;
appearance: revert;
max-width: revert;
max-height: revert;
color: revert;
font-family: revert;
font-size: revert;
font-weight: revert;
font-style: revert;
line-height: revert;
letter-spacing: revert;
text-transform: revert;
text-underline-offset: revert;
text-align: revert;
text-indent: revert;
text-shadow: revert;
vertical-align: revert;
transition-property: revert;
transition-timing-function: revert;
transition-delay: revert;
}
.kodza-testimonial-wall__eyebrow:not(#_):where(:not([hidden])),
.kodza-testimonial-wall__title:not(#_):where(:not([hidden])),
.kodza-testimonial-wall__ring:not(#_):where(:not([hidden])),
.kodza-testimonial-wall__icon:not(#_):where(:not([hidden])),
.kodza-testimonial-wall__quote:not(#_):where(:not([hidden])),
.kodza-testimonial-wall__quote p:where(:is(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))):not(.kodza-testimonial-wall__quote :not(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))) *)):not(#_):where(:not([hidden])) {
display: revert;
}
.kodza-testimonial-wall__eyebrow:not(#_):where([hidden]),
.kodza-testimonial-wall__title:not(#_):where([hidden]),
.kodza-testimonial-wall__ring:not(#_):where([hidden]),
.kodza-testimonial-wall__icon:not(#_):where([hidden]),
.kodza-testimonial-wall__quote:not(#_):where([hidden]),
.kodza-testimonial-wall__quote p:where(:is(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))):not(.kodza-testimonial-wall__quote :not(:not([class]), [class=""], [class^="is-"]:not([class*="kodza-"]):not([class*=" "]:not([class*=" is-"]))) *)):not(#_):where([hidden]) {
display: none;
}
/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */
.kodza-testimonial-wall__eyebrow:not(#_) {
margin: 0 0 18px;
font-family: var(--kz-font-label, var(--kz-testimonial-wall-mono));
font-size: var(--kz-font-label-size, 12.5px);
font-weight: var(--kz-font-label-weight, 500);
line-height: var(--kz-font-label-leading, inherit);
letter-spacing: var(--kz-font-label-tracking, 0.02em);
text-transform: var(--kz-font-label-case, inherit);
color: var(--kz-testimonial-wall-dim);
}
.kodza-testimonial-wall__title:not(#_) {
margin: 0;
font-family: var(--kz-font-display, inherit);
font-size: calc(clamp(32px, 4.6vw, 56px) * var(--kz-font-display-scale, 1));
font-weight: var(--kz-font-display-weight, 600);
line-height: 1.02;
letter-spacing: var(--kz-font-display-tracking, -0.045em);
text-transform: var(--kz-font-display-case, none);
color: inherit;
}
.kodza-testimonial-wall__pause:not(#_) {
position: relative;
flex: none;
display: grid;
place-items: center;
width: 40px;
height: 40px;
margin: 0;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--kz-testimonial-wall-ink);
cursor: pointer;
}
.kodza-testimonial-wall:not(.is-live) .kodza-testimonial-wall__pause:not(#_) {
visibility: hidden;
}
.kodza-testimonial-wall__pause:focus-visible:not(#_) {
outline: 2px solid var(--kz-testimonial-wall-ink);
outline-offset: 3px;
}
.kodza-testimonial-wall__ring:not(#_) {
transition: stroke calc(0.2s * var(--kz-motion-scale, 1));
}
.kodza-testimonial-wall__card:not(#_) {
box-sizing: border-box;
display: flex;
margin: 0;
padding: 28px;
border: 1px solid var(--kz-testimonial-wall-line);
border-radius: var(--kz-testimonial-wall-radius);
background: var(--kz-testimonial-wall-card);
transition: border-color calc(0.2s * var(--kz-motion-scale, 1));
}
@media (hover: hover) {
.kodza-testimonial-wall__card:hover:not(#_) {
border-color: var(--kz-testimonial-wall-line-strong);
}
}
.kodza-testimonial-wall__quote:not(#_) {
margin: 0;
padding: 0;
border: 0;
font-size: 15px;
line-height: 1.55;
}
.kodza-testimonial-wall__quote p:not(#_) {
margin: 0;
}
.kodza-testimonial-wall__who:not(#_) {
display: grid;
}
@container kodza-testimonial-wall (max-width: 560px) {
.kodza-testimonial-wall__card:not(#_) {
padding: 24px;
}
}
@media (prefers-reduced-motion: reduce) {
.kodza-testimonial-wall *:not(#_),
.kodza-testimonial-wall *:not(#_)::before,
.kodza-testimonial-wall *:not(#_)::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Testimonial Wall
// Deals the cards into columns (as many as --kz-testimonial-wall-columns asks at that width),
// repeats each column until it covers the window twice and slides it by exactly one set of cards,
// every other column downwards, so the loop never shows its seam. A column under the mouse eases
// to a stop; the pause button, the keyboard focus inside, a wall out of view or a hidden tab stop
// them all. With reduced motion the cards are dealt the same way and stay put.
// Settings: the --kz-testimonial-wall-* variables (speed, height, columns), and the constants below.
(() => {
const SELECTOR = '[data-kodza-testimonial-wall]';
const EASE = 260; // ms, how gently a column slows to a stop and gets going again
const OFFSETS = [0, 0.42, 0.71, 0.18]; // where each column starts in its loop, so no two rows line up
const MAX_SETS = 50; // copies of a column at most, for a very short one
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// The site's pace, --kz-motion-scale (set on :root, read on the instance): every duration of the
// script times it, and 0 moves nothing, like reduced motion
const paceOf = (el) => {
const pace = parseFloat(getComputedStyle(el).getPropertyValue('--kz-motion-scale'));
return pace >= 0 ? pace : 1;
};
// One instance, with its own state: two copies on a page never interfere. Everything it
// starts is tied to `signal` (listeners get { signal }, observers and timers stop on its
// 'abort' event), so kodza:destroy stops it all.
function mount(root, signal) {
const pace = paceOf(root);
const still = reduceMotion || !pace; // the still path: no motion asked, by the visitor or the site
const wall = root.querySelector('.kodza-testimonial-wall__wall');
const list = root.querySelector('.kodza-testimonial-wall__cards');
const button = root.querySelector('.kodza-testimonial-wall__pause');
const cards = list ? [...list.children] : [];
if (!wall || !cards.length) return;
let columns = [];
let count = 0;
let width = 0;
let visible = false;
let focused = false;
let held = false;
let frame = 0;
let pending = 0;
let last = 0;
const waiting = () => held || focused || !visible || document.hidden;
// A column's speed, eased: 1 runs, 0 holds (paused, so the browser stops drawing it)
const apply = (col) => {
const animation = col.animation;
if (!animation) return;
if (!col.rate) {
if (animation.playState === 'running') animation.pause();
return;
}
animation.playbackRate = col.rate;
if (animation.playState !== 'running') animation.play();
};
const tick = (now) => {
const step = last ? Math.min(64, now - last) : 16;
const share = 1 - Math.exp(-step / (EASE * pace));
let easing = false;
last = now;
columns.forEach((col) => {
const target = waiting() || col.hovered ? 0 : 1;
if (col.rate !== target) {
col.rate += (target - col.rate) * share;
if (Math.abs(target - col.rate) < 0.004) col.rate = target;
else easing = true;
}
apply(col);
});
frame = easing ? requestAnimationFrame(tick) : 0;
if (!easing) last = 0;
};
const sync = () => {
// Out of sight, nothing to ease: the columns simply hold, and ease in again when seen
if (!visible || document.hidden) {
cancelAnimationFrame(frame);
frame = 0;
last = 0;
columns.forEach((col) => {
col.rate = 0;
apply(col);
});
return;
}
if (!frame) frame = requestAnimationFrame(tick);
};
// Card 1 to the first column, card 2 to the second, and round again
const countOf = () => Math.max(1, Math.min(cards.length, Math.round(parseFloat(getComputedStyle(wall).getPropertyValue('--kz-testimonial-wall-columns'))) || 3));
const deal = () => {
columns.forEach((col) => col.animation?.cancel());
count = countOf();
columns = Array.from({ length: count }, () => {
const column = document.createElement('div');
const track = document.createElement('div');
column.className = 'kodza-testimonial-wall__column';
track.className = 'kodza-testimonial-wall__track';
column.append(track);
const col = { column, track, animation: null, duration: 0, rate: 0, hovered: false };
column.addEventListener('pointerenter', (event) => {
if (event.pointerType !== 'mouse') return;
col.hovered = true;
sync();
}, { signal });
column.addEventListener('pointerleave', () => {
col.hovered = false;
sync();
}, { signal });
return col;
});
cards.forEach((card, i) => columns[i % count].track.append(card));
list.replaceChildren(...columns.map((col) => col.column));
};
// Each column repeats its cards until it covers the window plus one set, then slides by exactly
// one set: the first copy lands where the cards were, so the seam is never seen
const loop = () => {
const speed = parseFloat(getComputedStyle(root).getPropertyValue('--kz-testimonial-wall-speed')) || 24;
const height = wall.clientHeight;
columns.forEach((col, i) => {
const { track } = col;
const progress = col.animation ? ((col.animation.currentTime || 0) % col.duration) / col.duration : OFFSETS[i % OFFSETS.length];
col.animation?.cancel();
track.querySelectorAll('[data-copy]').forEach((el) => el.remove());
const originals = [...track.children];
const copy = () => originals.forEach((card) => {
const twin = card.cloneNode(true);
twin.setAttribute('data-copy', '');
twin.setAttribute('aria-hidden', 'true');
twin.inert = true;
track.append(twin);
});
copy();
const set = track.children[originals.length].offsetTop - originals[0].offsetTop;
if (set <= 0) return;
for (let sets = 2; track.offsetHeight < height + set && sets < MAX_SETS; sets += 1) copy();
col.duration = (set / speed) * 1000 * pace;
col.animation = track.animate(
[{ transform: 'translate3d(0, 0, 0)' }, { transform: `translate3d(0, ${-set}px, 0)` }],
{ duration: col.duration, iterations: Infinity, direction: i % 2 ? 'reverse' : 'normal' },
);
col.animation.currentTime = progress * col.duration;
apply(col);
});
};
// A new width: dealt again when the number of columns changes, measured again in any case
const resize = new ResizeObserver(() => {
if (wall.clientWidth === width) return;
width = wall.clientWidth;
cancelAnimationFrame(pending);
pending = requestAnimationFrame(() => {
if (countOf() !== count) deal();
if (still) return;
loop();
sync();
});
});
const seen = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting;
sync();
});
signal.addEventListener('abort', () => {
resize.disconnect();
seen.disconnect();
cancelAnimationFrame(frame);
cancelAnimationFrame(pending);
columns.forEach((col) => col.animation?.cancel());
});
deal();
if (still) {
root.classList.add('is-still');
width = wall.clientWidth;
resize.observe(wall);
return;
}
root.classList.add('is-live');
button?.addEventListener('click', () => {
held = !held;
button.setAttribute('aria-pressed', String(held));
sync();
}, { signal });
// A link reached with the keyboard holds the wall; the pause button has its own say
root.addEventListener('focusin', (event) => {
focused = event.target !== button && event.target.matches(':focus-visible');
sync();
}, { signal });
root.addEventListener('focusout', (event) => {
if (!root.contains(event.relatedTarget)) focused = false;
sync();
}, { signal });
document.addEventListener('visibilitychange', sync, { signal });
// Measured once the fonts are in: a card's height depends on them
Promise.resolve(document.fonts?.ready).then(() => {
if (signal.aborted) return;
loop();
width = wall.clientWidth;
resize.observe(wall);
seen.observe(wall);
});
}
// Kodza lifecycle, the same in every component: `kodza:init` starts the instances inside
// event.detail.root (the whole page by default), `kodza:destroy` stops them and puts their
// markup back. For themes and apps that swap content without a page load. Each running root
// keeps its markup as written under KODZA, shared by every Kodza script on the page: a Kodza
// component inside another one is saved as written, not half-started, and stopped with it. Two
// components on one root (a reveal on a stat card) both start, and stop together.
const KODZA = Symbol.for('kodza');
const RUNNING = Symbol.for(`kodza ${SELECTOR}`); // a root this component runs on, for every copy of this script
const instances = new WeakMap();
const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
const attrsOf = (el) => [...el.attributes].map((a) => [a.name, a.value]);
const put = (el, saved) => {
[...el.attributes].forEach((a) => el.removeAttribute(a.name));
saved.attrs.forEach(([name, value]) => el.setAttribute(name, value));
el.innerHTML = saved.html;
};
// The markup inside a root as written: Kodza components already running in it give back their own
const written = (root) => {
const all = [...root.querySelectorAll('*')];
if (!all.some((el) => el[KODZA])) return root.innerHTML;
const copy = root.cloneNode(true);
const twins = copy.querySelectorAll('*');
let last = null;
all.forEach((el, i) => {
if (!el[KODZA] || last?.contains(el)) return;
last = el;
put(twins[i], el[KODZA]);
});
return copy.innerHTML;
};
const init = (scope) => find(scope).forEach((root) => {
// Already running, maybe from another copy of this script. Marked is-ready with no Kodza component
// running on it, a root is a copy of a started one (a cloned slide): it is left as it is
if (root[RUNNING] || (root.classList.contains('is-ready') && !root[KODZA])) return;
// Rendered by React (data-kodza-manual): React starts it, with an init aimed at that markup once
// it has hydrated it. Any earlier start would change markup React has not taken over yet
const owner = root.closest('[data-kodza-manual]');
if (owner && !(scope && owner.contains(scope))) return;
const controller = new AbortController();
// Another Kodza component already running on this root: its markup as written is kept, and the
// two stop together
const before = root[KODZA];
const saved = { html: before ? before.html : written(root), attrs: before ? before.attrs : attrsOf(root), stop: () => destroy(root) };
if (before) before.above = saved;
instances.set(root, { controller, saved, before });
root[KODZA] = saved;
root[RUNNING] = true;
mount(root, controller.signal);
root.classList.add('is-ready');
});
const destroy = (scope) => find(scope).forEach((root) => {
const instance = instances.get(root);
if (!instance) return;
instances.delete(root);
instance.saved.above?.stop(); // a Kodza component started after it on the same root stops first
// The Kodza components running inside it stop first: once its markup is put back, their own
// script could not find them any more
root.querySelectorAll('*').forEach((el) => el[KODZA]?.stop());
instance.controller.abort();
delete root[RUNNING];
// Started on top of another one: that one stops next, and puts the markup back
if (instance.before) {
delete instance.before.above;
root[KODZA] = instance.before;
return instance.before.stop();
}
delete root[KODZA];
put(root, instance.saved);
});
document.addEventListener('kodza:init', (event) => init(event.detail?.root));
document.addEventListener('kodza:destroy', (event) => destroy(event.detail?.root));
init();
// Loaded in <head>, the script runs before the markup exists: once more when it is parsed
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init(), { once: true });
})();
'use client';
// Kodza — Testimonial Wall, for React. From https://kodza.dev/components/testimonial-wall/
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Next to this file: testimonial-wall.css and testimonial-wall.js, the component's CSS and JS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
// The script changes this markup once React has rendered it (it adds layers, splits text, moves
// nodes), so React must not patch it afterwards. When a prop changes, the key gives React a fresh
// element: kodza:destroy stops the script on the old one, kodza:init starts it on the new one.
// data-kodza-manual keeps the script from starting it on its own: the effect below does, once
// React has hydrated the markup (on a page that streams in, the script can load first).
// Built from your data (tabs, rows, options)? Put what the markup is made of in the key, as plain
// values (ids, text, numbers): a change starts the script again on fresh markup. What the visitor
// changes (the open tab, the sort, what they type) is the script's: keep it out of the key, follow
// it with onClick or onInput on the root, and change it from your code as a visitor would (click
// the tab; set the field's value, then dispatch a bubbling input event).
import { useRef, useEffect, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './testimonial-wall.css';
export type TestimonialWallProps = Omit<ComponentPropsWithoutRef<'section'>, "children" | "vars"> & {
/** Settings, as CSS variables on the root */
vars?: {
/** default: #0a0a0a */
"--kz-testimonial-wall-ink"?: string | number;
/** default: rgba(10, 10, 10, 0.64) */
"--kz-testimonial-wall-dim"?: string | number;
/** default: #ffffff */
"--kz-testimonial-wall-card"?: string | number;
/** default: rgba(10, 10, 10, 0.1) */
"--kz-testimonial-wall-line"?: string | number;
/** the pointed card — default: rgba(10, 10, 10, 0.42) */
"--kz-testimonial-wall-line-strong"?: string | number;
/** default: #e6e6e3 */
"--kz-testimonial-wall-face"?: string | number;
/** default: rgba(10, 10, 10, 0.16) */
"--kz-testimonial-wall-track"?: string | number;
/** default: transparent */
"--kz-testimonial-wall-bg"?: string | number;
/** 2 under 860px, 1 under 560px (below) — default: 3 */
"--kz-testimonial-wall-columns"?: string | number;
/** the window the columns drift through — default: 640px */
"--kz-testimonial-wall-height"?: string | number;
/** pixels per second — default: 24 */
"--kz-testimonial-wall-speed"?: string | number;
/** default: 112px */
"--kz-testimonial-wall-fade"?: string | number;
/** default: 16px */
"--kz-testimonial-wall-gap"?: string | number;
/** default: 1200px */
"--kz-testimonial-wall-width"?: string | number;
/** default: clamp(64px, 8vw, 112px) */
"--kz-testimonial-wall-pad-y"?: string | number;
/** default: clamp(16px, 4vw, 40px) */
"--kz-testimonial-wall-gutter"?: string | number;
/** default: calc(16px * var(--kz-radius-scale, 1)) */
"--kz-testimonial-wall-radius"?: string | number;
/** default: var(--kz-font-mono, "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace) */
"--kz-testimonial-wall-mono"?: string | number;
/** default: cubic-bezier(0.22, 1, 0.36, 1) */
"--kz-testimonial-wall-ease"?: string | number;
};
className?: string;
};
export function TestimonialWall({ vars, className, style, ...rest }: TestimonialWallProps) {
const ref = useRef<ComponentRef<'section'>>(null);
const classes = ["kodza-testimonial-wall", className].filter(Boolean).join(' ');
// Everything the markup is made of: when it changes, React gets a fresh element (see the top)
const key = JSON.stringify([classes, vars, style, rest]);
// Starts the script on this element once React has rendered it, stops it when it goes
useEffect(() => {
const root = ref.current;
let live = true;
// @ts-ignore: a plain script, no types to find
import('./testimonial-wall.js').then(() => {
if (live) document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }));
});
return () => {
live = false;
document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } }));
};
}, [key]);
return (
<section data-kodza-testimonial-wall="" key={key} {...rest} className={classes} data-kodza-manual style={{ ...vars, ...style } as CSSProperties} ref={ref}>
<div className="kodza-testimonial-wall__inner">
<div className="kodza-testimonial-wall__head">
<div className="kodza-testimonial-wall__heading">
<p className="kodza-testimonial-wall__eyebrow">Customers</p>
<h2 className="kodza-testimonial-wall__title">Sent. Paid. Forgotten.</h2>
</div>
<button className="kodza-testimonial-wall__pause" type="button" aria-pressed="false" aria-label="Pause the testimonials">
<svg className="kodza-testimonial-wall__ring" viewBox="0 0 40 40" aria-hidden="true">
<circle cx="20" cy="20" r="19.25" />
</svg>
<svg className="kodza-testimonial-wall__icon" viewBox="0 0 16 16" aria-hidden="true">
<path className="kodza-testimonial-wall__bars" d="M5.5 4v8M10.5 4v8" />
<path className="kodza-testimonial-wall__play" d="M5.5 3.8v8.4L12 8z" />
</svg>
</button>
</div>
<div className="kodza-testimonial-wall__wall">
<div className="kodza-testimonial-wall__cards">
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>I send invoices from my phone between two fittings. Most clients have paid before I'm back at the counter.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">CL</span>
<span className="kodza-testimonial-wall__name">Claire Lebrun</span>
<span className="kodza-testimonial-wall__role">Owner, Atelier Lebrun · Le Havre</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>The API docs open on a curl that works. We took our first payment the same afternoon.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">TF</span>
<span className="kodza-testimonial-wall__name">Tomás Ferreira</span>
<span className="kodza-testimonial-wall__role">Developer, Ondula · Porto</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>We moved 4,000 customers off our old billing in one weekend. Nobody noticed, which was the whole point.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">JW</span>
<span className="kodza-testimonial-wall__name">Jonas Weber</span>
<span className="kodza-testimonial-wall__role">CTO, Kollektiv Nord · Berlin</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>Reminders go out on their own, politely. I used to write those at midnight, less politely.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">AB</span>
<span className="kodza-testimonial-wall__name">Aïcha Benali</span>
<span className="kodza-testimonial-wall__role">Illustrator · Montréal</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>My accountant asked what changed. The exports: clean CSV, the right columns, nothing to retype.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">CD</span>
<span className="kodza-testimonial-wall__name">Camille Dubreuil</span>
<span className="kodza-testimonial-wall__role">Manager, Maison Ternes · Nantes</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>No sales call to get an API key. I signed up during stand-up and it was live before stand-up ended.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">LH</span>
<span className="kodza-testimonial-wall__name">Lucas Hartmann</span>
<span className="kodza-testimonial-wall__role">Lead developer, Studio Rapp · Ghent</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>VAT in four countries, done right, and no more spreadsheet called FINAL_v7.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">SM</span>
<span className="kodza-testimonial-wall__name">Sofia Marques</span>
<span className="kodza-testimonial-wall__role">Finance, Lume Ceramics · Lisbon</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>It does three things and does them properly. I've paid for tools that did forty things badly.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">PV</span>
<span className="kodza-testimonial-wall__name">Pieter de Vries</span>
<span className="kodza-testimonial-wall__role">Founder, Kade Studio · Rotterdam</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>Support answered with a fix and a link to the changelog. Not a ticket number.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">HL</span>
<span className="kodza-testimonial-wall__name">Hugo Lemaire</span>
<span className="kodza-testimonial-wall__role">Freelance developer · Marseille</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>We switched the bakery's card reader and invoices on a Monday. By Friday I'd stopped thinking about it.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">AR</span>
<span className="kodza-testimonial-wall__name">Ana Ruiz</span>
<span className="kodza-testimonial-wall__role">Owner, Forn Ruiz · Bilbao</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>Webhooks retry, sign their payload and show you the response. Basic, apparently rare.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">MC</span>
<span className="kodza-testimonial-wall__name">Matteo Conti</span>
<span className="kodza-testimonial-wall__role">Back-end developer, Officina 14 · Turin</span>
</figcaption>
</figure>
<figure className="kodza-testimonial-wall__card">
<blockquote className="kodza-testimonial-wall__quote">
<p>Payouts land on day two, every time. Cash flow stopped being a weekly guessing game.</p>
</blockquote>
<figcaption className="kodza-testimonial-wall__who">
<span className="kodza-testimonial-wall__face" aria-hidden="true">FL</span>
<span className="kodza-testimonial-wall__name">Freya Lindqvist</span>
<span className="kodza-testimonial-wall__role">Co-founder, Halvø Coffee · Copenhagen</span>
</figcaption>
</figure>
</div>
</div>
</div>
</section>
);
}
Checked
Robots poked at it before you did. Every component goes through the same checks, and again each time it changes.