Marketing
Testimonials Wall
Responsive customer proof wall with ratings, quotes, roles, and company context.
Built with Proa
Teams move faster when the UI stops fighting back.
From first prototype to mature design system, Proa gives product teams a clearer path to the interface they meant to ship.
5 out of 5 stars
★★★★★
We replaced a fragile set of templates with a component system the whole Rust team can actually improve. The first migration shipped in days.
The source-first model is the rare promise that gets better as your product becomes more opinionated.
“Design systems
Typed recipes made our design reviews concrete. Designers can point to a token, engineers can point to an API, and both sides stay in sync.
Our marketing pages got lighter, our product surfaces got more consistent, and we stopped debugging hydration for content that never needed it.
SSRRust
Proa let us keep the ergonomics our frontend developers expect while moving the rendering path somewhere far easier to reason about.
Release velocity
2.4×
More product surfaces shipped per quarter after consolidating on one component system.
FreeTestimonials Wall
Responsive customer proof wall with ratings, quotes, roles, and company context.
Browse block sourcetestimonials_wall.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: (MIT OR Apache-2.0) AND MIT
// Adapted from MIT-licensed sources:
// - Tailark, `packages/dusk-kit/blocks/testimonials/six/index.tsx`
// https://github.com/tailark/blocks @ 8eadeb3389ccccc4bfc4a10bca959c1b39e47b71 (MIT License)
// - Tailark, `packages/mist-kit/blocks/testimonials/two/index.tsx`
// https://github.com/tailark/blocks @ 8eadeb3389ccccc4bfc4a10bca959c1b39e47b71 (MIT License)
// - TWBlocks, `blocks/testimonials/testimonials1.tsx`
// https://github.com/tommyjepsen/twblocks @ ff88cb951dab0dc01198b6570ec5f5cf21250d45 (MIT License)
//! Testimonials wall block — a responsive social-proof grid with customer identities.
use proa_core::{DataLoader, WebContext, WebRenderSync, WriteBuf, WriteError};
use proa_macros::html_sync;
use proa_ui::{
Avatar, AvatarFallback, AvatarShape, AvatarSize, Badge, BadgeVariant, Card, CardContent,
};
/// The flourish above a quote. The wall deliberately varies these so the grid
/// does not read as six copies of one card.
enum Topper {
None,
Testimonials Wall source
Browse and copy the reviewed source included with this Free block.
testimonials_wall.rsOpen raw ↗
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: (MIT OR Apache-2.0) AND MIT
// Adapted from MIT-licensed sources:
// - Tailark, `packages/dusk-kit/blocks/testimonials/six/index.tsx`
// https://github.com/tailark/blocks @ 8eadeb3389ccccc4bfc4a10bca959c1b39e47b71 (MIT License)
// - Tailark, `packages/mist-kit/blocks/testimonials/two/index.tsx`
// https://github.com/tailark/blocks @ 8eadeb3389ccccc4bfc4a10bca959c1b39e47b71 (MIT License)
// - TWBlocks, `blocks/testimonials/testimonials1.tsx`
// https://github.com/tommyjepsen/twblocks @ ff88cb951dab0dc01198b6570ec5f5cf21250d45 (MIT License)
//! Testimonials wall block — a responsive social-proof grid with customer identities.
use proa_core::{DataLoader, WebContext, WebRenderSync, WriteBuf, WriteError};
use proa_macros::html_sync;
use proa_ui::{
Avatar, AvatarFallback, AvatarShape, AvatarSize, Badge, BadgeVariant, Card, CardContent,
};
/// The flourish above a quote. The wall deliberately varies these so the grid
/// does not read as six copies of one card.
enum Topper {
None,
/// A five-star rating, with the rating also stated for screen readers.
Stars,
/// An oversized quote mark paired with a category badge.
Category(&'static str),
/// Short technology tags.
Tags(&'static [&'static str]),
}
/// Light card on the page background, or the one inverted card that anchors the
/// grid. Every colour that has to flip lives here rather than being repeated as
/// a per-card class string.
#[derive(Clone, Copy, PartialEq)]
enum Tone {
Light,
Inverted,
}
impl Tone {
fn card(self) -> &'static str {
match self {
Self::Light => "border-border/80 bg-background shadow-none",
Self::Inverted => "border-border/80 bg-primary text-primary-foreground shadow-none",
}
}
fn quote(self) -> &'static str {
match self {
Self::Light => "text-base leading-7 text-foreground",
Self::Inverted => "text-lg font-medium leading-8",
}
}
fn avatar(self) -> Option<&'static str> {
match self {
Self::Light => None,
Self::Inverted => Some("ring-2 ring-primary-foreground/20"),
}
}
fn cite(self) -> &'static str {
match self {
Self::Light => "not-italic text-sm font-semibold text-foreground",
Self::Inverted => "not-italic text-sm font-semibold",
}
}
fn role(self) -> &'static str {
match self {
Self::Light => "text-xs text-muted-foreground",
Self::Inverted => "text-xs text-primary-foreground/70",
}
}
}
struct Testimonial {
topper: Topper,
tone: Tone,
quote: &'static str,
initials: &'static str,
/// Avatar fill, spelled out in full for Tailwind's source scan.
avatar_tint: &'static str,
name: &'static str,
role: &'static str,
}
impl<B: WriteBuf, L: DataLoader> WebRenderSync<L, B> for &Testimonial {
fn render(self, cx: &mut WebContext<L, B>) -> Result<(), WriteError> {
// Only the star rating pushes the quote down; the other toppers carry
// their own bottom margin.
let quote_spacing = if matches!(self.topper, Topper::Stars) {
"mt-5"
} else {
""
};
html_sync! {
{Card {
class: Some(self.tone.card()),
children: Some(CardContent {
class: Some("pt-6"),
children: Some(html_sync! {
{match &self.topper {
Topper::None => {}
Topper::Stars => html_sync! {
<span class="sr-only">"5 out of 5 stars"</span>
<div aria-hidden="true" class="text-sm tracking-[0.2em] text-amber-500">
"★★★★★"
</div>
}.render(cx)?,
Topper::Category(label) => html_sync! {
<div class="mb-5 flex items-center justify-between">
<span class="text-3xl font-serif text-primary">"“"</span>
{Badge { variant: BadgeVariant::Secondary, class: Some("rounded-full"), children: Some(*label), }}
</div>
}.render(cx)?,
Topper::Tags(tags) => html_sync! {
<div class="mb-5 flex gap-2">
{for tag in *tags {
html_sync! {
<span class="rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
{*tag}
</span>
}
}}
</div>
}.render(cx)?,
}}
<blockquote class={proa_macros::text!("{} {}", quote_spacing, self.tone.quote())}>
{self.quote}
</blockquote>
<footer class="mt-6 flex items-center gap-3">
{Avatar {
size: AvatarSize::Lg,
shape: AvatarShape::Circle,
class: self.tone.avatar(),
image: None::<()>,
fallback: Some(AvatarFallback {
size: AvatarSize::Lg,
class: Some(self.avatar_tint),
children: Some(self.initials),
}),
}}
<div>
<cite class={self.tone.cite()}>{self.name}</cite>
<p class={self.tone.role()}>{self.role}</p>
</div>
</footer>
}),
}),
}}
}
.render(cx)
}
}
/// One entry per masonry column. The columns are uneven on purpose, so this is a
/// slice of slices rather than a flat list chunked at render time.
const COLUMNS: &[&[Testimonial]] = &[
&[
Testimonial {
topper: Topper::Stars,
tone: Tone::Light,
quote: "We replaced a fragile set of templates with a component system the whole Rust team can actually improve. The first migration shipped in days.",
initials: "AK",
avatar_tint: "bg-primary/10 text-primary",
name: "Ari Kim",
role: "Engineering lead, Lantern",
},
Testimonial {
topper: Topper::None,
tone: Tone::Inverted,
quote: "The source-first model is the rare promise that gets better as your product becomes more opinionated.",
initials: "MP",
avatar_tint: "bg-primary-foreground/15 text-primary-foreground",
name: "Mina Patel",
role: "Founder, Incline",
},
],
&[
Testimonial {
topper: Topper::Category("Design systems"),
tone: Tone::Light,
quote: "Typed recipes made our design reviews concrete. Designers can point to a token, engineers can point to an API, and both sides stay in sync.",
initials: "JL",
avatar_tint: "bg-violet-500/10 text-violet-600 dark:text-violet-400",
name: "Jordan Lee",
role: "Design director, Thread",
},
Testimonial {
topper: Topper::None,
tone: Tone::Light,
quote: "Our marketing pages got lighter, our product surfaces got more consistent, and we stopped debugging hydration for content that never needed it.",
initials: "SC",
avatar_tint: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
name: "Sam Chen",
role: "Staff engineer, Northstar",
},
],
&[Testimonial {
topper: Topper::Tags(&["SSR", "Rust"]),
tone: Tone::Light,
quote: "Proa let us keep the ergonomics our frontend developers expect while moving the rendering path somewhere far easier to reason about.",
initials: "RN",
avatar_tint: "bg-sky-500/10 text-sky-600 dark:text-sky-400",
name: "Riley Novak",
role: "CTO, Fieldwork",
}],
];
const COLUMN_CLASSES: &[&str] = &[
"space-y-4",
"space-y-4 lg:pt-8",
"space-y-4 md:col-span-2 lg:col-span-1",
];
/// Copy-paste-ready block composition.
pub struct TestimonialsWallBlock;
impl<B: proa_core::WriteBuf, L: proa_core::DataLoader> proa_core::WebRenderSync<L, B> for TestimonialsWallBlock {
fn render(
self,
cx: &mut proa_core::WebContext<L, B>,
) -> Result<(), proa_core::WriteError> {
html_sync! {
<div class="flex flex-col gap-6">
<section
data-block="testimonials-wall"
class="overflow-hidden rounded-3xl border border-border bg-muted/30 px-5 py-14 md:px-8 md:py-20 lg:px-12"
>
<div class="mx-auto max-w-5xl">
<div class="mx-auto max-w-2xl text-center">
{Badge { variant: BadgeVariant::Outline, class: Some("rounded-full bg-background"), children: Some("Built with Proa"), }}
<h2 class="mt-5 text-balance text-3xl font-semibold tracking-tight text-foreground md:text-5xl">
"Teams move faster when the UI stops fighting back."
</h2>
<p class="mx-auto mt-5 max-w-xl text-pretty text-lg leading-8 text-muted-foreground">
"From first prototype to mature design system, Proa gives product teams a clearer path to the interface they meant to ship."
</p>
</div>
<div class="mt-12 grid items-start gap-4 md:grid-cols-2 lg:grid-cols-3">
{for (index, column) in COLUMNS.iter().enumerate() {
html_sync! {
<div class={COLUMN_CLASSES[index]}>
{for testimonial in *column {
html_sync! { {testimonial} }
}}
{if index + 1 == COLUMNS.len() {
// The result card closes the wall: a number rather
// than a quote, so it stays hand-written instead of
// being bent into the Testimonial shape.
html_sync! {
{Card { class: Some("overflow-hidden border-border/80 bg-zinc-950 text-zinc-50 shadow-none"), children: Some(CardContent { class: Some("relative pt-6"), children: Some(html_sync! {
<div aria-hidden="true" class="absolute -right-8 -top-8 size-32 rounded-full bg-primary/30 blur-3xl"></div>
<p class="relative text-xs font-semibold uppercase tracking-[0.2em] text-zinc-400">
"Release velocity"
</p>
<p class="relative mt-3 text-4xl font-semibold">"2.4×"</p>
<p class="relative mt-3 text-sm leading-6 text-zinc-400">
"More product surfaces shipped per quarter after consolidating on one component system."
</p>
<div class="relative mt-6 flex items-end gap-1.5">
<span class="h-5 flex-1 rounded-xs bg-zinc-800"></span>
<span class="h-8 flex-1 rounded-xs bg-zinc-700"></span>
<span class="h-12 flex-1 rounded-xs bg-primary/70"></span>
<span class="h-16 flex-1 rounded-xs bg-primary"></span>
</div>
}), }), }}
}.render(cx)?;
}}
</div>
}
}}
</div>
</div>
</section>
</div>
}
.render(cx)
}
}