Testimonials Wall
A dense wall-of-love — a heading above a multi-column masonry of compact testimonial cards, each with author, optional role, and avatar.
import type { Block } from 'payload'import { testimonialFields, testimonialItemFields } from '@/blocks/shared/testimonialFields'export const TestimonialsWall: Block = { slug: 'testimonialsWall', interfaceName: 'TestimonialsWallBlock', fields: [ // Shared testimonials heading (eyebrow, title, description). Edit the shared // shape in @/blocks/shared/testimonialFields to update every variant. ...testimonialFields, { name: 'testimonials', type: 'array', required: true, minRows: 1, maxRows: 24, admin: { initCollapsed: true, }, // Shared one-quote shape — see @/blocks/shared/testimonialFields. fields: testimonialItemFields, }, ], labels: { plural: 'Testimonials Wall Blocks', singular: 'Testimonials Wall', },}import React from 'react'import type { TestimonialsWallBlock as TestimonialsWallBlockData } from '@/payload-types'import { Media } from '@/components/Media'import { Badge } from '@/components/ui/badge'import { cn } from '@/utilities/ui'// Layout adapted from tailark/blocks (MIT) — re-implemented as a Payload block.type Props = TestimonialsWallBlockData & { id?: string className?: string disableInnerContainer?: boolean}export const TestimonialsWallBlock: React.FC<Props> = ({ className, description, disableInnerContainer, eyebrow, id, testimonials, title,}) => { return ( <section className={cn('container', className)} id={id ? `block-${id}` : undefined}> <div className="overflow-hidden rounded-frame border border-border/70 bg-card/35 px-6 py-10 sm:px-8 lg:px-12 lg:py-14"> <div className={cn('flex flex-col gap-10', { 'mx-auto max-w-5xl': !disableInnerContainer, })} > <div className="flex flex-col items-center gap-4 text-center"> {eyebrow ? ( <Badge variant="outline" className="rounded-full px-3 py-1 uppercase tracking-eyebrow"> {eyebrow} </Badge> ) : null} <h2 className="text-3xl font-medium tracking-title text-balance sm:text-4xl">{title}</h2> {description ? ( <p className="max-w-2xl text-base leading-7 text-muted-foreground">{description}</p> ) : null} </div> {testimonials && testimonials.length > 0 ? ( <div className="columns-1 gap-4 sm:columns-2 lg:columns-3"> {testimonials.map((item, index) => ( <figure className="mb-4 flex break-inside-avoid flex-col gap-3 rounded-2xl border border-border/70 bg-background/60 p-5" key={item.id ?? index} > <figcaption className="flex items-center gap-3"> {item.avatar ? ( <div className="flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-full border border-border/70 bg-card"> <Media resource={item.avatar} imgClassName="size-full object-cover" /> </div> ) : null} <div className="flex flex-col"> <span className="text-sm font-medium text-foreground">{item.author}</span> {item.role ? ( <span className="text-xs text-muted-foreground">{item.role}</span> ) : null} </div> </figcaption> <blockquote className="text-pretty text-sm leading-6 text-muted-foreground"> {item.quote} </blockquote> </figure> ))} </div> ) : null} </div> </div> </section> )}import type { Field } from 'payload'/** * Shared field core for the Testimonials component family. * * Every testimonials variant spreads `testimonialFields` first for the shared * section heading (eyebrow + title + intro), then appends its own * variant-specific shape — a grid of cards, a star rating, a bento, or a dense * wall. Editing the shared heading here updates every installed testimonials * block at once, so the family never drifts field-by-field across a repo. * * `testimonialItemFields` is the one-quote shape (quote, author, optional role, * optional avatar upload) reused everywhere a testimonial appears — as the array * items in the grid/rating/bento/wall variants, and as the single subject of the * quote/spotlight variants — so a testimonial looks the same wherever it shows. * Each avatar is an editable Media upload, so editors manage social proof from * the admin instead of shipping hardcoded image URLs. * * Installed once per repo at `src/blocks/shared/testimonialFields.ts`; re-running * `payload-components add testimonials-*` never overwrites a copy you have * already edited. */export const testimonialFields: Field[] = [ { name: 'eyebrow', type: 'text', }, { name: 'title', type: 'text', required: true, }, { name: 'description', type: 'textarea', },]export const testimonialItemFields: Field[] = [ { name: 'quote', type: 'textarea', required: true, }, { name: 'author', type: 'text', required: true, }, { name: 'role', type: 'text', }, { name: 'avatar', type: 'upload', relationTo: 'media', },]Installation
npx payload-components add testimonials-wallCopy the files straight from the registry, then wire the Payload fragments by hand:
pnpm dlx shadcn@latest add https://payload-components.xyz/r/testimonials-wall.jsonWhat it installs
Copies 3 source files into your project:
src/blocks/shared/testimonialFields.tssharedsrc/blocks/TestimonialsWall/config.tssrc/blocks/TestimonialsWall/Component.tsx
…and makes 4 edits to wire the block into your project:
| Registers the block | src/collections/Pages/index.ts |
| Maps the renderer | src/blocks/RenderBlocks.tsx |
| Regenerates types | src/payload-types.ts |
| Regenerates the admin import map | src/app/(payload)/admin/importMap.js |
testimonialFields.ts is the shared field core for this family — every variant composes it. Editing it updates each installed block at once, and re-running an install never overwrites a copy you have changed.Re-running the install converges: it detects existing wiring, skips it, and records install state in .payload-components/state.json.
Content model
The eyebrow, title, and description heading fields come from the shared
Testimonials family base (src/blocks/shared/testimonialFields.ts); the testimonials
wall is specific to this variant.
Prop
Type
Each item in testimonials carries the shared one-quote shape:
Prop
Type
Usage
TestimonialsWall block to its layout.RenderBlocks on the frontend, fully typed — no extra wiring.Requirements
- Target
- payload-website-starter
- Payload
- v3
- Next.js
- 15 / 16
- shadcn UI
- badge
Your project must already expose components.json, src/payload.config.ts, src/blocks/RenderBlocks.tsx, src/collections/Pages/index.ts — the surfaces payload-components add patches. The CLI verifies this against the support matrix before touching anything.
In this family
testimonials-gridtestimonials-bentotestimonials-wallcurrenttestimonials-ratingtestimonials-spotlighttestimonials-quote