Testimonial

A customer quote with author details

Testimonial shows a quote large enough to carry a section, with the author's avatar, name and role underneath. Quotation marks are added for you. The home page has one.

Installation

Install the dependencies.

npm install @kushagradhawan/kookie-ui

Copy this file into components/blocks/testimonial. Keep the folder names, since blocks import each other by relative path.

components/blocks/testimonial/testimonial.tsx
import * as React from 'react';
import { Avatar, Flex, Heading, Text } from '@kushagradhawan/kookie-ui';
import type { AvatarProps, FlexProps, HeadingProps, TextProps } from '@kushagradhawan/kookie-ui';

const TestimonialRoot = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ direction = 'column', align = 'center', justify = 'center', gap = '6', ...props }, ref) => (
    <Flex ref={ref} direction={direction} align={align} justify={justify} gap={gap} {...props} />
  ),
);
TestimonialRoot.displayName = 'Testimonial.Root';

/** The quote. Pass the words only; quotation marks are added. */
const TestimonialQuote = React.forwardRef<HTMLHeadingElement, HeadingProps>(
  ({ size = '7', align = 'center', weight = 'medium', children, ...props }, ref) => (
    <Heading ref={ref} size={size} align={align} weight={weight} {...props}>
      “{children}”
    </Heading>
  ),
);
TestimonialQuote.displayName = 'Testimonial.Quote';

/** Groups the avatar and details. */
const TestimonialAuthor = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ direction = 'column', align = 'center', gap = '2', ...props }, ref) => (
    <Flex ref={ref} direction={direction} align={align} gap={gap} {...props} />
  ),
);
TestimonialAuthor.displayName = 'Testimonial.Author';

const TestimonialAvatar = React.forwardRef<HTMLImageElement, AvatarProps>(
  ({ size = '4', radius = 'full', ...props }, ref) => <Avatar ref={ref} size={size} radius={radius} {...props} />,
);
TestimonialAvatar.displayName = 'Testimonial.Avatar';

/** Groups the name and role. */
const TestimonialDetails = React.forwardRef<HTMLDivElement, FlexProps>(
  ({ direction = 'column', align = 'center', gap = '0', ...props }, ref) => (
    <Flex ref={ref} direction={direction} align={align} gap={gap} {...props} />
  ),
);
TestimonialDetails.displayName = 'Testimonial.Details';

const TestimonialName = React.forwardRef<HTMLSpanElement, TextProps>(({ size = '3', weight = 'medium', ...props }, ref) => (
  <Text ref={ref} size={size} weight={weight} {...props} />
));
TestimonialName.displayName = 'Testimonial.Name';

const TestimonialRole = React.forwardRef<HTMLSpanElement, TextProps>(({ size = '2', emphasis = 'medium', ...props }, ref) => (
  <Text ref={ref} size={size} emphasis={emphasis} {...props} />
));
TestimonialRole.displayName = 'Testimonial.Role';

export const Testimonial = Object.assign(TestimonialRoot, {
  Root: TestimonialRoot,
  Quote: TestimonialQuote,
  Author: TestimonialAuthor,
  Avatar: TestimonialAvatar,
  Details: TestimonialDetails,
  Name: TestimonialName,
  Role: TestimonialRole,
});

Usage

import { Testimonial } from '@/components/blocks/testimonial/testimonial';
 
<Testimonial.Root>
  <Testimonial.Quote>We shipped the redesign two weeks early.</Testimonial.Quote>
  <Testimonial.Author>
    <Testimonial.Avatar src="/people/sam.jpg" fallback="SK" />
    <Testimonial.Details>
      <Testimonial.Name>Sam Kim</Testimonial.Name>
      <Testimonial.Role>Design lead, Acme</Testimonial.Role>
    </Testimonial.Details>
  </Testimonial.Author>
</Testimonial.Root>

Anatomy

<Testimonial.Root>
  <Testimonial.Quote />
  <Testimonial.Author>
    <Testimonial.Avatar />
    <Testimonial.Details>
      <Testimonial.Name />
      <Testimonial.Role />
    </Testimonial.Details>
  </Testimonial.Author>
</Testimonial.Root>

Props

Every part accepts the props of the component it wraps. Only changed defaults are listed.

PartWrapsDefaults
RootFlexdirection="column", align="center", justify="center", gap="6"
QuoteHeadingsize="7", weight="medium", align="center". Wraps children in quotation marks
AuthorFlexdirection="column", align="center", gap="2"
AvatarAvatarsize="4", radius="full"
DetailsFlexdirection="column", align="center", gap="0"
NameTextsize="3", weight="medium"
RoleTextsize="2", emphasis="medium"

Examples

Centered Quote

“We shipped the redesign two weeks early, and nobody had to learn a new system.”

Sam KimDesign lead, Acme
<Testimonial.Root>
  <Testimonial.Quote size="5">
    We shipped the redesign two weeks early, and nobody had to learn a new system.
  </Testimonial.Quote>
  <Testimonial.Author>
    <Testimonial.Avatar fallback="SK" color="gray" />
    <Testimonial.Details>
      <Testimonial.Name>Sam Kim</Testimonial.Name>
      <Testimonial.Role>Design lead, Acme</Testimonial.Role>
    </Testimonial.Details>
  </Testimonial.Author>
</Testimonial.Root>

Left Aligned

Set align="start" on the root, author and details, and align="left" on the quote.

<Testimonial.Root align="start">
  <Testimonial.Quote align="left">…</Testimonial.Quote>
  <Testimonial.Author align="start">
    <Testimonial.Avatar src="/people/sam.jpg" fallback="SK" />
    <Testimonial.Details align="start">
      <Testimonial.Name>Sam Kim</Testimonial.Name>
      <Testimonial.Role>Design lead, Acme</Testimonial.Role>
    </Testimonial.Details>
  </Testimonial.Author>
</Testimonial.Root>

Accessibility

Quote renders a heading, which suits a quote that leads a section. For a quote inside running content, wrap it in a blockquote or use Kookie's Blockquote instead. The avatar's fallback shows initials; give it an alt when you pass src.

© 2026 Kushagra Dhawan. Licensed under MIT. GitHub.

Theme

Accent color

Gray color

Appearance

Radius

Scaling

Panel background