Home / Snippets / UI Components /

Testimonial card

Customer quote card with decorative quotation mark via ::before, avatar initials, and role — no JavaScript.

"The CSS techniques I learned here completely changed how I approach styling. Clean, modern, and no framework bloat in sight."

Alex Johnson
Senior Frontend Engineer
Widely Supported
uino-js

Quick implementation

/* HTML:
<article class="testi-card">
  <p class="testi-quote">"Your quote text here."</p>
  <footer class="testi-footer">
    <div class="testi-avatar" aria-hidden="true">AJ</div>
    <div>
      <div class="testi-name">Alex Johnson</div>
      <div class="testi-role">Senior Frontend Engineer</div>
    </div>
  </footer>
</article> */

.testi-card {
  position: relative;
  background: var(--card);
  border-radius: 0.75rem;
  padding: 2rem 2rem 1.5rem;
  max-width: 26rem;
}

.testi-card::before {
  content: '\201C';
  position: absolute;
  top: 0.5rem;
  left: 1.25rem;
  font-family: Georgia, serif;
  font-size: 5rem;
  line-height: 1;
  color: oklch(0.72 0.19 265);
  opacity: 0.35;
  pointer-events: none;
  user-select: none;
}

.testi-quote {
  position: relative;
  font-size: 1rem;
  line-height: 1.65;
  color: var(--text);
  margin: 0 0 1.5rem;
  padding-top: 1rem;
}

.testi-footer {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.testi-avatar {
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  background: oklch(0.52 0.22 265);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 1rem;
  color: oklch(1 0 0);
  flex-shrink: 0;
}

.testi-name {
  font-weight: 600;
  color: var(--text);
  font-size: 0.9rem;
}

.testi-role {
  color: var(--muted);
  font-size: 0.8rem;
  margin-top: 0.1rem;
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building UI components for a dark-mode design system.

Goal: A customer testimonial card with a decorative large quotation mark, a paragraph of quote text, and a footer with an avatar circle showing initials, a name, and a role — pure CSS, no JavaScript.

Technical constraints:
- Use ::before on the card to render the opening curly quote (\201C) as a decorative large typographic mark.
- The ::before quote mark must be positioned absolutely and sit behind the quote text via z-index or pointer-events: none.
- Avatar is a circle created with border-radius: 50%, using oklch() background color and white initials.
- Use oklch() for all color values, including semi-transparent accents via oklch(... / alpha).
- All layout uses flexbox — no grid needed for the card itself.
- Use semantic HTML: article for the card, footer for attribution, p for the quote.

Framework variant (pick one):
A) Vanilla HTML + CSS only — single self-contained component.
B) React component — accept quote (string), name (string), role (string), and initials (string) props; derive avatar background from a hash of the name prop.

Edge cases to handle:
- Long quotes should not overflow the card — allow the text to wrap naturally.
- Missing avatar image: always fall back to initials-only circle (no broken img tags).
- Cards in a grid: ensure equal heights with align-items: stretch on the grid container.
- Screen readers: mark the decorative ::before quote mark with aria-hidden via pointer-events and user-select: none.

Return HTML + CSS.

Why this matters in 2026

Social proof is one of the most persuasive UI patterns on the web, and a well-crafted testimonial card earns its place by communicating authenticity. The decorative quotation mark — rendered as a CSS ::before pseudo-element rather than a background image or SVG — keeps the markup clean and the asset count at zero. Building this pattern without JavaScript means it renders instantly, works in every environment, and has no hydration cost in server-rendered frameworks.

The logic

The large opening quotation mark is the Unicode character \201C inserted via content on .testi-card::before. It is positioned absolutely in the top-left corner, scaled to 5rem, and kept visually behind the quote text by setting pointer-events: none and a low opacity. The quote paragraph uses padding-top: 1rem to clear the decorative mark visually. The footer uses display: flex with align-items: center to align the avatar circle with the name-role stack, and flex-shrink: 0 on the avatar prevents it from collapsing on narrow widths.

Accessibility & performance

The ::before quotation mark is decorative only — it carries no semantic meaning because the quote text already contains the actual punctuation. Setting pointer-events: none and user-select: none prevents the glyph from being accidentally selected or interacted with. The avatar initials circle uses aria-hidden="true" so screen readers skip the redundant initials and announce the adjacent name text instead. Because the entire component is CSS-only — no images for the quote mark or avatar background — there are zero additional network requests per card.