25 CSS Testimonials & Reviews07 / 25

Pure CSSMIT licensed

Before & After Case Study Quote Card

The layout agencies and B2B consultancies convert on: a client quote sitting shoulder to shoulder with the numbers that back it up. The delta figures count up on load using @property plus counter() — genuine style-engine interpolation, no JavaScript — and the before/after rows use a diagonal strike so the improvement is legible in one glance.

Published

Live Demo
Try it

The code

<section class="tr-07" aria-label="Before and after case study quote card demo">
  <div class="tr-07__stage">
    <article class="tr-07__card">
      <div class="tr-07__quote">
        <p class="tr-07__tag">Case study · D2C retail</p>
        <span class="tr-07__mark" aria-hidden="true">“</span>
        <blockquote>
          <p>We had eleven tools and no answers. Nine weeks later we had one pipeline, and the forecast was accurate enough that we stopped hedging inventory. That is the whole story — the rest is arithmetic.</p>
        </blockquote>
        <figcaption class="tr-07__who">
          <img src="https://images.unsplash.com/photo-1560250097-0b93528c311a?q=80&w=140&h=140&fit=crop&auto=format" alt="Portrait of Nathan Boateng" width="52" height="52" loading="lazy" decoding="async">
          <span><b>Nathan Boateng</b>Chief Revenue Officer · Coplandt &amp; Sons</span>
        </figcaption>
        <p class="tr-07__foot">Measured Jan–Sep 2026 against the preceding nine months. Methodology in the full case study.</p>
      </div>

      <div class="tr-07__stats">
        <div class="tr-07__stat">
          <b class="tr-07__big" data-k="a"><span class="tr-07__pre">+</span><span class="tr-07__n" aria-hidden="true"></span><span class="tr-07__suf">%</span></b>
          <span class="tr-07__sr">Plus 310 percent</span>
          <p>Monthly recurring revenue</p>
        </div>
        <div class="tr-07__stat">
          <b class="tr-07__big" data-k="b"><span class="tr-07__pre">−</span><span class="tr-07__n" aria-hidden="true"></span><span class="tr-07__suf">%</span></b>
          <span class="tr-07__sr">Minus 64 percent</span>
          <p>Cost per acquisition</p>
        </div>

        <dl class="tr-07__rows">
          <div>
            <dt>Time to forecast</dt>
            <dd class="tr-07__old">9 days</dd>
            <dd class="tr-07__new">40 min</dd>
          </div>
          <div>
            <dt>Tools in the stack</dt>
            <dd class="tr-07__old">11</dd>
            <dd class="tr-07__new">3</dd>
          </div>
          <div>
            <dt>Stockouts per quarter</dt>
            <dd class="tr-07__old">27</dd>
            <dd class="tr-07__new">2</dd>
          </div>
          <div>
            <dt>Analyst hours / week</dt>
            <dd class="tr-07__old">62</dd>
            <dd class="tr-07__new">9</dd>
          </div>
        </dl>
      </div>
    </article>
    <p class="tr-07__chip" aria-hidden="true">@property &lt;integer&gt; · counter-reset var() · content:counter() · grid rails · tabular-nums</p>
  </div>
</section>
@property --tr-07-a {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

@property --tr-07-b {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

.tr-07,
.tr-07 *,
.tr-07 *::before,
.tr-07 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tr-07 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --tr-07-bg: oklch(0.21 0.032 268);
  --tr-07-card: oklch(0.98 0.006 250);
  --tr-07-ink: oklch(0.22 0.02 265);
  --tr-07-mut: oklch(0.54 0.02 265);
  --tr-07-acc: oklch(0.68 0.17 155);
  background: var(--tr-07-bg);
  color: var(--tr-07-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing: antialiased;
}

.tr-07__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 20px;
  padding: clamp(18px,4vw,54px);
  background: radial-gradient(85% 60% at 20% 0%,color-mix(in oklch,var(--tr-07-acc) 16%,transparent),transparent 62%);
}

.tr-07__card {
  container-type: inline-size;
  width: min(100%,1020px);
  display: grid;
  background: var(--tr-07-card);
  border-radius: 24px;
  overflow: hidden;
  box-shadow: 0 40px 90px -50px oklch(0 0 0/.85);
  animation: tr-07-in .8s cubic-bezier(.16,1,.3,1) both;
}

@keyframes tr-07-in {
  from {
    opacity: 0;
    translate: 0 22px;
  }

  to {
    opacity: 1;
    translate: 0 0;
  }
}

@container (min-width: 760px) {
  .tr-07__card {
    grid-template-columns: 1.15fr .85fr;
  }
}

.tr-07__quote {
  position: relative;
  padding: clamp(26px,4cqi,44px);
  display: grid;
  align-content: center;
  gap: 20px;
}

.tr-07__tag {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: color-mix(in oklch,var(--tr-07-acc) 72%,black);
}

.tr-07__mark {
  position: absolute;
  inset-block-start: clamp(6px,2cqi,18px);
  inset-inline-end: clamp(14px,3cqi,34px);
  font-family: Georgia,'Times New Roman',serif;
  font-size: clamp(120px,18cqi,190px);
  line-height: 1;
  color: color-mix(in oklch,var(--tr-07-acc) 16%,transparent);
  pointer-events: none;
}

.tr-07__quote blockquote p {
  font-size: clamp(18px,2.5cqi,25px);
  line-height: 1.42;
  letter-spacing: -.028em;
  font-weight: 520;
  text-wrap: pretty;
  position: relative;
}

.tr-07__who {
  display: flex;
  align-items: center;
  gap: 13px;
}

.tr-07__who img {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  background: oklch(0.86 0.02 250);
}

.tr-07__who span {
  display: grid;
  font-size: 13px;
  line-height: 1.4;
  color: var(--tr-07-mut);
}

.tr-07__who b {
  font-size: 15.5px;
  font-weight: 660;
  color: var(--tr-07-ink);
  letter-spacing: -.018em;
}

.tr-07__foot {
  font-size: 11.5px;
  line-height: 1.5;
  color: color-mix(in oklch,var(--tr-07-mut) 85%,transparent);
  max-width: 46ch;
}

.tr-07__stats {
  padding: clamp(24px,4cqi,40px);
  display: grid;
  align-content: center;
  gap: clamp(16px,2.4cqi,24px);
  background: linear-gradient(170deg,color-mix(in oklch,var(--tr-07-acc) 13%,var(--tr-07-card)),color-mix(in oklch,var(--tr-07-acc) 4%,var(--tr-07-card)));
  border-inline-start: 1px solid oklch(0 0 0/.07);
}

.tr-07__stat p {
  font-size: 12.5px;
  color: var(--tr-07-mut);
  margin-top: 2px;
  letter-spacing: -.005em;
}

.tr-07__big {
  display: flex;
  align-items: baseline;
  font-size: clamp(40px,7cqi,60px);
  font-weight: 720;
  line-height: 1;
  letter-spacing: -.05em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  color: color-mix(in oklch,var(--tr-07-acc) 80%,black);
}

.tr-07__suf,
.tr-07__pre {
  font-size: .55em;
  font-weight: 640;
  color: color-mix(in oklch,var(--tr-07-acc) 70%,black);
}

.tr-07__n::after {
  content: counter(tr-07-c);
}

.tr-07__big[data-k='a'] .tr-07__n {
  counter-reset: tr-07-c var(--tr-07-a);
  animation: tr-07-run-a 2.1s cubic-bezier(.16,1,.3,1) .15s both;
}

.tr-07__big[data-k='b'] .tr-07__n {
  counter-reset: tr-07-c var(--tr-07-b);
  animation: tr-07-run-b 2.1s cubic-bezier(.16,1,.3,1) .3s both;
}

@keyframes tr-07-run-a {
  from {
    --tr-07-a: 0;
  }

  to {
    --tr-07-a: 310;
  }
}

@keyframes tr-07-run-b {
  from {
    --tr-07-b: 0;
  }

  to {
    --tr-07-b: 64;
  }
}

.tr-07__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.tr-07__rows {
  display: grid;
  gap: 1px;
  margin-top: 6px;
  background: oklch(0 0 0/.08);
  border: 1px solid oklch(0 0 0/.08);
  border-radius: 12px;
  overflow: hidden;
}

.tr-07__rows>div {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 12px;
  padding: 11px 14px;
  background: var(--tr-07-card);
}

.tr-07__rows dt {
  font-size: 12.5px;
  color: var(--tr-07-mut);
}

.tr-07__rows dd {
  font-size: 13.5px;
  font-weight: 620;
  font-variant-numeric: tabular-nums;
}

.tr-07__old {
  color: color-mix(in oklch,var(--tr-07-mut) 90%,transparent);
  text-decoration: line-through 2px;
  text-decoration-color: oklch(0.6 0.19 25/.75);
}

.tr-07__new {
  position: relative;
  padding-inline-start: 20px;
  color: color-mix(in oklch,var(--tr-07-acc) 78%,black);
}

.tr-07__new::before {
  content: '→';
  position: absolute;
  inset-inline-start: 0;
  color: var(--tr-07-mut);
  font-weight: 500;
}

.tr-07__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  color: oklch(0.78 0.02 265);
  padding: 8px 15px;
  border: 1px solid oklch(1 0 0/.14);
  border-radius: 99px;
  text-align: center;
}

@supports not (background: paint(x)) {
  .tr-07__n::after {
    content: counter(tr-07-c);
  }
}

@media (prefers-reduced-motion: reduce) {
  .tr-07__card {
    animation: none;
  }

  .tr-07__n {
    animation-duration: 1ms;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Testimonials & Review from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Before & After Case Study Quote Card
Source: https://codefronts.com/components/css-testimonials-reviews/before-and-after-case-study-quote-card/

The layout agencies and B2B consultancies convert on: a client quote sitting shoulder to shoulder with the numbers that back it up. The delta figures count up on load using @property plus counter() — genuine style-engine interpolation, no JavaScript — and the before/after rows use a diagonal strike so the improvement is legible in one glance.
## HTML
```html
<section class="tr-07" aria-label="Before and after case study quote card demo">
  <div class="tr-07__stage">
    <article class="tr-07__card">
      <div class="tr-07__quote">
        <p class="tr-07__tag">Case study · D2C retail</p>
        <span class="tr-07__mark" aria-hidden="true">“</span>
        <blockquote>
          <p>We had eleven tools and no answers. Nine weeks later we had one pipeline, and the forecast was accurate enough that we stopped hedging inventory. That is the whole story — the rest is arithmetic.</p>
        </blockquote>
        <figcaption class="tr-07__who">
          <img src="https://images.unsplash.com/photo-1560250097-0b93528c311a?q=80&w=140&h=140&fit=crop&auto=format" alt="Portrait of Nathan Boateng" width="52" height="52" loading="lazy" decoding="async">
          <span><b>Nathan Boateng</b>Chief Revenue Officer · Coplandt &amp; Sons</span>
        </figcaption>
        <p class="tr-07__foot">Measured Jan–Sep 2026 against the preceding nine months. Methodology in the full case study.</p>
      </div>

      <div class="tr-07__stats">
        <div class="tr-07__stat">
          <b class="tr-07__big" data-k="a"><span class="tr-07__pre">+</span><span class="tr-07__n" aria-hidden="true"></span><span class="tr-07__suf">%</span></b>
          <span class="tr-07__sr">Plus 310 percent</span>
          <p>Monthly recurring revenue</p>
        </div>
        <div class="tr-07__stat">
          <b class="tr-07__big" data-k="b"><span class="tr-07__pre">−</span><span class="tr-07__n" aria-hidden="true"></span><span class="tr-07__suf">%</span></b>
          <span class="tr-07__sr">Minus 64 percent</span>
          <p>Cost per acquisition</p>
        </div>

        <dl class="tr-07__rows">
          <div>
            <dt>Time to forecast</dt>
            <dd class="tr-07__old">9 days</dd>
            <dd class="tr-07__new">40 min</dd>
          </div>
          <div>
            <dt>Tools in the stack</dt>
            <dd class="tr-07__old">11</dd>
            <dd class="tr-07__new">3</dd>
          </div>
          <div>
            <dt>Stockouts per quarter</dt>
            <dd class="tr-07__old">27</dd>
            <dd class="tr-07__new">2</dd>
          </div>
          <div>
            <dt>Analyst hours / week</dt>
            <dd class="tr-07__old">62</dd>
            <dd class="tr-07__new">9</dd>
          </div>
        </dl>
      </div>
    </article>
    <p class="tr-07__chip" aria-hidden="true">@property &lt;integer&gt; · counter-reset var() · content:counter() · grid rails · tabular-nums</p>
  </div>
</section>
```
## CSS
```css
@property --tr-07-a {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

@property --tr-07-b {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

.tr-07,
.tr-07 *,
.tr-07 *::before,
.tr-07 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.tr-07 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --tr-07-bg: oklch(0.21 0.032 268);
  --tr-07-card: oklch(0.98 0.006 250);
  --tr-07-ink: oklch(0.22 0.02 265);
  --tr-07-mut: oklch(0.54 0.02 265);
  --tr-07-acc: oklch(0.68 0.17 155);
  background: var(--tr-07-bg);
  color: var(--tr-07-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  -webkit-font-smoothing: antialiased;
}

.tr-07__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 20px;
  padding: clamp(18px,4vw,54px);
  background: radial-gradient(85% 60% at 20% 0%,color-mix(in oklch,var(--tr-07-acc) 16%,transparent),transparent 62%);
}

.tr-07__card {
  container-type: inline-size;
  width: min(100%,1020px);
  display: grid;
  background: var(--tr-07-card);
  border-radius: 24px;
  overflow: hidden;
  box-shadow: 0 40px 90px -50px oklch(0 0 0/.85);
  animation: tr-07-in .8s cubic-bezier(.16,1,.3,1) both;
}

@keyframes tr-07-in {
  from {
    opacity: 0;
    translate: 0 22px;
  }

  to {
    opacity: 1;
    translate: 0 0;
  }
}

@container (min-width: 760px) {
  .tr-07__card {
    grid-template-columns: 1.15fr .85fr;
  }
}

.tr-07__quote {
  position: relative;
  padding: clamp(26px,4cqi,44px);
  display: grid;
  align-content: center;
  gap: 20px;
}

.tr-07__tag {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: color-mix(in oklch,var(--tr-07-acc) 72%,black);
}

.tr-07__mark {
  position: absolute;
  inset-block-start: clamp(6px,2cqi,18px);
  inset-inline-end: clamp(14px,3cqi,34px);
  font-family: Georgia,'Times New Roman',serif;
  font-size: clamp(120px,18cqi,190px);
  line-height: 1;
  color: color-mix(in oklch,var(--tr-07-acc) 16%,transparent);
  pointer-events: none;
}

.tr-07__quote blockquote p {
  font-size: clamp(18px,2.5cqi,25px);
  line-height: 1.42;
  letter-spacing: -.028em;
  font-weight: 520;
  text-wrap: pretty;
  position: relative;
}

.tr-07__who {
  display: flex;
  align-items: center;
  gap: 13px;
}

.tr-07__who img {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  background: oklch(0.86 0.02 250);
}

.tr-07__who span {
  display: grid;
  font-size: 13px;
  line-height: 1.4;
  color: var(--tr-07-mut);
}

.tr-07__who b {
  font-size: 15.5px;
  font-weight: 660;
  color: var(--tr-07-ink);
  letter-spacing: -.018em;
}

.tr-07__foot {
  font-size: 11.5px;
  line-height: 1.5;
  color: color-mix(in oklch,var(--tr-07-mut) 85%,transparent);
  max-width: 46ch;
}

.tr-07__stats {
  padding: clamp(24px,4cqi,40px);
  display: grid;
  align-content: center;
  gap: clamp(16px,2.4cqi,24px);
  background: linear-gradient(170deg,color-mix(in oklch,var(--tr-07-acc) 13%,var(--tr-07-card)),color-mix(in oklch,var(--tr-07-acc) 4%,var(--tr-07-card)));
  border-inline-start: 1px solid oklch(0 0 0/.07);
}

.tr-07__stat p {
  font-size: 12.5px;
  color: var(--tr-07-mut);
  margin-top: 2px;
  letter-spacing: -.005em;
}

.tr-07__big {
  display: flex;
  align-items: baseline;
  font-size: clamp(40px,7cqi,60px);
  font-weight: 720;
  line-height: 1;
  letter-spacing: -.05em;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  color: color-mix(in oklch,var(--tr-07-acc) 80%,black);
}

.tr-07__suf,
.tr-07__pre {
  font-size: .55em;
  font-weight: 640;
  color: color-mix(in oklch,var(--tr-07-acc) 70%,black);
}

.tr-07__n::after {
  content: counter(tr-07-c);
}

.tr-07__big[data-k='a'] .tr-07__n {
  counter-reset: tr-07-c var(--tr-07-a);
  animation: tr-07-run-a 2.1s cubic-bezier(.16,1,.3,1) .15s both;
}

.tr-07__big[data-k='b'] .tr-07__n {
  counter-reset: tr-07-c var(--tr-07-b);
  animation: tr-07-run-b 2.1s cubic-bezier(.16,1,.3,1) .3s both;
}

@keyframes tr-07-run-a {
  from {
    --tr-07-a: 0;
  }

  to {
    --tr-07-a: 310;
  }
}

@keyframes tr-07-run-b {
  from {
    --tr-07-b: 0;
  }

  to {
    --tr-07-b: 64;
  }
}

.tr-07__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.tr-07__rows {
  display: grid;
  gap: 1px;
  margin-top: 6px;
  background: oklch(0 0 0/.08);
  border: 1px solid oklch(0 0 0/.08);
  border-radius: 12px;
  overflow: hidden;
}

.tr-07__rows>div {
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: center;
  gap: 12px;
  padding: 11px 14px;
  background: var(--tr-07-card);
}

.tr-07__rows dt {
  font-size: 12.5px;
  color: var(--tr-07-mut);
}

.tr-07__rows dd {
  font-size: 13.5px;
  font-weight: 620;
  font-variant-numeric: tabular-nums;
}

.tr-07__old {
  color: color-mix(in oklch,var(--tr-07-mut) 90%,transparent);
  text-decoration: line-through 2px;
  text-decoration-color: oklch(0.6 0.19 25/.75);
}

.tr-07__new {
  position: relative;
  padding-inline-start: 20px;
  color: color-mix(in oklch,var(--tr-07-acc) 78%,black);
}

.tr-07__new::before {
  content: '→';
  position: absolute;
  inset-inline-start: 0;
  color: var(--tr-07-mut);
  font-weight: 500;
}

.tr-07__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  color: oklch(0.78 0.02 265);
  padding: 8px 15px;
  border: 1px solid oklch(1 0 0/.14);
  border-radius: 99px;
  text-align: center;
}

@supports not (background: paint(x)) {
  .tr-07__n::after {
    content: counter(tr-07-c);
  }
}

@media (prefers-reduced-motion: reduce) {
  .tr-07__card {
    animation: none;
  }

  .tr-07__n {
    animation-duration: 1ms;
  }
}
```

How this works

CSS cannot animate text, but it can animate a registered custom property, and a counter can print one:

@property --tr-07-a {
  syntax: '<integer>'; inherits: false; initial-value: 0;
}
.stat b {
  counter-reset: n var(--tr-07-a);
  animation: run 2s cubic-bezier(.16,1,.3,1) both;
}
.stat b::after { content: counter(n); }
@keyframes run { to { --tr-07-a: 310; } }

The syntax descriptor is the whole trick: an unregistered custom property is an untyped string and can only swap discretely. Registering it as <integer> teaches the engine to interpolate and round every frame.

The split is a two-column grid that collapses at a container query, and the two halves share a baseline because the stat column uses align-content: center rather than stretching. The before/after rows are a three-column subgrid — label, old value, new value — so every row aligns to the same rails regardless of digit count:

.rows { display: grid; grid-template-columns: 1fr auto auto; }
.old { text-decoration: line-through 2px;
       text-decoration-color: color-mix(in oklch, var(--ink) 40%, transparent); }

Accessibility: animated digits are aria-hidden and each stat carries a visually-hidden span with the real final value, so a screen reader hears "310 percent increase in monthly recurring revenue" exactly once rather than three hundred intermediate numbers.

Make it yours

  • Different ease per stat: cubic-bezier(.16,1,.3,1) lands fast and hard; steps(24) gives a deliberately mechanical tick that suits financial figures.
  • Decimals need two counters — CSS counters are integers only. Animate the whole and fractional parts separately and put a literal . between them.
  • Add a logo lockup above the quote with filter: grayscale(1) that clears on hover — restrained, and it keeps client brands from fighting your palette.
  • Swap load-triggered animation for scroll-triggered by adding animation-timeline: view(); animation-range: entry 20% cover 40% inside an @supports block.

Gotchas — read before shipping

  • Without @property registration the number snaps from 0 to the target in one frame — the animation silently does nothing useful.
  • counter() output is not selectable, searchable or usefully exposed to assistive tech. Always pair it with a real text node carrying the true value.
  • Digits of different widths make the row twitch as the number climbs. font-variant-numeric: tabular-nums is mandatory here, not optional.
  • Under prefers-reduced-motion set the animation duration to 1 ms rather than nonenone freezes the counter at zero and hides the data.
  • Case-study metrics are marketing claims. Cite the measurement window and methodology in the footnote; unsubstantiated performance claims are actionable in US, UK and AU advertising law.

Browser support

ChromeSafariFirefoxEdge
114+17.5+128+114+

Floor set by oklch(), color-mix(), @property, @container, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

@property is the floor: Chrome 85, Safari 16.4, Firefox 128 (July 2024). Older Firefox shows the initial value; the visually-hidden real number and an @supports fallback that prints the static figure keep the card correct and honest everywhere.

Search CodeFronts

Loading…