Skip to content
← Blocks

Testimonial Grid

A complete testimonial grid section, ready to adapt to your product.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/testimonial-grid.json

Use it

Scroll horizontally for long lines
"use client";
import { TestimonialGrid } from "@/components/jez-ui/blocks/testimonial-grid";

export default function Example() {
  return (
    <>
      <TestimonialGrid />
    </>
  );
}

Compose your own

These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.

Scroll horizontally for long lines
"use client";
import {
  TestimonialGrid,
  TestimonialGridHeading,
  TestimonialGridLead,
  TestimonialGridQuotes,
} from "@/components/jez-ui/blocks/testimonial-grid";

export default function Example() {
  return (
    <TestimonialGrid>
      <TestimonialGridHeading />
      <TestimonialGridLead />
      <TestimonialGridQuotes />
    </TestimonialGrid>
  );
}
Scroll horizontally for long lines
import { TestimonialGrid, TestimonialGridTitle, TestimonialGridDescription, TestimonialGridContent, TestimonialGridHeading, TestimonialGridLead, TestimonialGridQuotes, TestimonialGridQuote, TestimonialGridQuotation, TestimonialGridAttribution } from "@/components/jez-ui/blocks/testimonial-grid";
Read the composition guide →

Props & types

Scroll horizontally for long lines
export type TestimonialGridOptions = {
  className?: string;
  items?: typeof TestimonialGridDefaultItems;
  heading?: React.ReactNode;
  description?: React.ReactNode;
};

export type TestimonialGridProps = Omit<
  React.ComponentProps<"section">,
  keyof TestimonialGridOptions
> &
  TestimonialGridOptions;

TestimonialGridProps

Source

View 2 source files

registry/blocks/testimonial-grid.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "../ui/utils";
export type TestimonialGridOptions = {
  className?: string;
  items?: typeof TestimonialGridDefaultItems;
  heading?: React.ReactNode;
  description?: React.ReactNode;
};
export type TestimonialGridProps = Omit<
  React.ComponentProps<"section">,
  keyof TestimonialGridOptions
> &
  TestimonialGridOptions;
const TestimonialGridDefaultItems = [
  "The best part is knowing where everything belongs.",
  "It gave our small team a little more room to think.",
  "A calmer way to get from the first idea to the finished thing.",
];
function useTestimonialGridModel({
  items = TestimonialGridDefaultItems,
  heading = "Room for your customers’ stories.",
  description = "Sample quotes for layout demonstration.",
  className,
  children,
  ...rootProps
}: TestimonialGridProps) {
  return { items, heading, description, className, children, rootProps };
}
const TestimonialGridCompositionContext = React.createContext<ReturnType<
  typeof useTestimonialGridModel
> | null>(null);
function useTestimonialGridComposition() {
  const context = React.useContext(TestimonialGridCompositionContext);
  if (!context)
    throw new Error("TestimonialGrid parts must be inside TestimonialGrid.");
  return context;
}
export function TestimonialGrid(props: TestimonialGridProps) {
  const model = useTestimonialGridModel(props);
  const { className, rootProps, children } = model;
  return (
    <TestimonialGridCompositionContext.Provider value={model}>
      <section {...rootProps} className={cn("py-8", className)}>
        {children !== undefined ? (
          children
        ) : (
          <>
            <TestimonialGridHeading />
            <TestimonialGridLead />
            <TestimonialGridQuotes />
          </>
        )}
      </section>
    </TestimonialGridCompositionContext.Provider>
  );
}

export function TestimonialGridTitle({
  className,
  ...props
}: React.ComponentProps<"h2">) {
  return (
    <h2
      data-slot="testimonial-grid-title"
      className={cn("text-3xl", className)}
      {...props}
    />
  );
}
export function TestimonialGridDescription({
  className,
  ...props
}: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="testimonial-grid-description"
      className={cn("mt-2 mb-7 text-sm text-muted-foreground", className)}
      {...props}
    />
  );
}
export function TestimonialGridContent({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="testimonial-grid-content"
      className={cn("grid gap-8 md:grid-cols-3", className)}
      {...props}
    />
  );
}

export function TestimonialGridHeading({
  children,
  ...props
}: Partial<React.ComponentProps<typeof TestimonialGridTitle>> & {
  children?: React.ReactNode;
}) {
  const { heading } = useTestimonialGridComposition();
  return (
    <TestimonialGridTitle {...props}>
      {children === undefined ? heading : children}
    </TestimonialGridTitle>
  );
}
export function TestimonialGridLead({
  children,
  ...props
}: Partial<React.ComponentProps<typeof TestimonialGridDescription>> & {
  children?: React.ReactNode;
}) {
  const { description } = useTestimonialGridComposition();
  return (
    <TestimonialGridDescription {...props}>
      {children === undefined ? description : children}
    </TestimonialGridDescription>
  );
}
export function TestimonialGridQuotes({
  children,
  ...props
}: Partial<React.ComponentProps<typeof TestimonialGridContent>> & {
  children?: React.ReactNode;
}) {
  const { items } = useTestimonialGridComposition();
  return (
    <TestimonialGridContent {...props}>
      {children === undefined
        ? items.map((q, i) => (
            <TestimonialGridQuote key={q}>
              <TestimonialGridQuotation>“{q}”</TestimonialGridQuotation>
              <TestimonialGridAttribution>
                Sample customer {i + 1} · Illustrative quote
              </TestimonialGridAttribution>
            </TestimonialGridQuote>
          ))
        : children}
    </TestimonialGridContent>
  );
}

export function TestimonialGridQuote({
  className,
  ...props
}: React.ComponentProps<"figure">) {
  return (
    <figure
      data-slot="testimonial-grid-quote"
      className={cn("m-0 border-t border-border pt-5", className)}
      {...props}
    />
  );
}
export function TestimonialGridQuotation({
  className,
  ...props
}: React.ComponentProps<"blockquote">) {
  return (
    <blockquote
      data-slot="testimonial-grid-quotation"
      className={cn("text-xl leading-relaxed", className)}
      {...props}
    />
  );
}
export function TestimonialGridAttribution({
  className,
  ...props
}: React.ComponentProps<"figcaption">) {
  return (
    <figcaption
      data-slot="testimonial-grid-attribution"
      className={cn("mt-5 text-xs text-muted-foreground", className)}
      {...props}
    />
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

Dependencies

clsx@2.1.1 · tailwind-merge@3.5.0

Accessibility & behaviour

Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.

Read the accessibility and performance guide →