Skip to content
Facade UI
Colour mode
GitHub (opens in a new tab)
blocksection

Testimonial — single

One quote given the whole band, optionally beside a portrait or over a tinted surface.

Preview

Open full width (opens in a new tab)
Preview width

Installation

Package manager
pnpm dlx shadcn@latest add https://facadeui.dev/r/testimonial-single.json

Pulls in container, section, testimonial, types, utils. The CLI installs them for you.

Usage

The exact source of the preview above.

demos/testimonial-single.tsx
import { TestimonialSingle } from "@registry/sections/testimonial-single"

import { QUOTES } from "./content"

export function Demo() {
  return <TestimonialSingle title="Customer story" item={QUOTES[0]!} variant="muted" />
}

Source

What the CLI copies into your project, byte for byte.

components/sections/testimonial-single.tsx
/**
 * TestimonialSingle — one quote, given the whole band.
 *
 * The featured counterpart to `TestimonialsGrid`: a single quote at display
 * size, optionally beside a portrait or over a tinted surface.
 *
 * a11y: as with every testimonial, the quote is a `<blockquote>` and the
 * attribution a `<figcaption>` outside it. The company logo, when given, is
 * `alt=""` — the company name is already in the attribution text, so announcing
 * both would be noise.
 *
 * Dependencies: react, @/lib/types, @/lib/utils,
 * @/components/ui/container, @/components/ui/section, @/components/ui/testimonial.
 */

import type { ElementType, ReactNode } from "react"

import type { ImageComponent, SectionBaseProps } from "@/lib/types"
import { cn, slugId } from "@/lib/utils"
import { Container } from "@/components/ui/container"
import { Section } from "@/components/ui/section"
import { Testimonial, type TestimonialItem } from "@/components/ui/testimonial"

export interface TestimonialSingleProps extends Omit<SectionBaseProps, "headingLevel"> {
  item: TestimonialItem
  image?: ImageComponent
  /** Visually hidden section name, so the band can still be a landmark. */
  title?: string
  /** Company logo shown above the quote. Decorative. */
  logoSrc?: string
  logoAlt?: string
  /** Portrait or media beside the quote. */
  media?: ReactNode
  variant?: "plain" | "muted" | "card"
  align?: "start" | "center"
  children?: ReactNode
}

const surfaces = {
  plain: "",
  muted: "bg-muted rounded-2xl px-6 py-12 sm:px-12",
  card: "bg-card text-card-foreground rounded-2xl border px-6 py-12 sm:px-12",
} as const

export function TestimonialSingle({
  item,
  image,
  title,
  logoSrc,
  logoAlt = "",
  media,
  variant = "plain",
  align = "center",
  as,
  spacing = "md",
  className,
  id,
  children,
}: TestimonialSingleProps) {
  const Image = (image ?? "img") as ElementType
  const titleId = title ? (id ? `${id}-title` : slugId(title)) : undefined

  return (
    <Section
      as={as ?? (title ? "section" : "div")}
      spacing={spacing}
      labelledBy={titleId}
      id={id}
      className={className}
    >
      <Container size={media ? "lg" : "md"}>
        {title ? (
          <h2 id={titleId} className="sr-only">
            {title}
          </h2>
        ) : null}

        <div
          className={cn(
            surfaces[variant],
            media &&
              "grid items-center gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]",
          )}
        >
          <div
            className={cn(
              "flex flex-col gap-8",
              align === "center" && !media && "items-center",
            )}
          >
            {logoSrc ? (
              <Image
                src={logoSrc}
                alt={logoAlt}
                loading="lazy"
                decoding="async"
                className="h-7 w-auto object-contain opacity-70"
              />
            ) : null}

            <Testimonial
              {...item}
              image={image}
              size="lg"
              variant="plain"
              className={cn(align === "center" && !media && "items-center text-center")}
            />

            {children}
          </div>

          {media ? <div className="min-w-0">{media}</div> : null}
        </div>
      </Container>
    </Section>
  )
}

Props

TestimonialSingleProps

Extends Omit<SectionBaseProps, "headingLevel">.

Props for TestimonialSingleProps
PropTypeDefault
item*RequiredTestimonialItem
imageImageComponent
titleVisually hidden section name, so the band can still be a landmark.string
logoSrcCompany logo shown above the quote. Decorative.string
logoAltstring""
mediaPortrait or media beside the quote.ReactNode
variant"plain" | "muted" | "card""plain"
align"start" | "center""center"
childrenReactNode