Tessli

Honest social proof

Shows only proof a reader can check, real quotes with a name and a link to the source, and public records instead when there is none yet.

  • Section
  • Social proof
  • Community

Community — not yet reviewed by a maintainer

Preview

Theme
Width
State

1440 px wide. Open this preview on its own ↗

Install

npx shadcn@latest add online-scope-lab/tessli/social-proof-honest

Run it in a project set up with shadcn/ui. It adds social-proof-honest.tsx to your components folder, and the item takes on your project’s theme.

When to use it

Use when

  • You have one to three quotes that named people agreed, in writing, to have published with their name and role.
  • Each quote already exists somewhere a reader can follow, such as a case study, the person's own post or a recorded talk.
  • The product is new and has no quotes yet, but has public records (changelog, source code, status history) a reader can check.

Avoid when

  • You have no permissioned quotes and no public record to point at; leave the section out rather than fill it.
  • You want to show usage numbers or customer logos; publish those only with a cited source and written permission, in their own section.
  • The quotes are long stories; link to full case studies from a short list instead.

Common AI default vs. this

The common default

A "Trusted by" strip of grey placeholder logos, three counters such as "10,000+ teams" and "4.9/5 rating", and a grid of identical testimonial cards with five stars, a stock avatar, a first name and a vague line of praise, none of it traceable to a real person or source.

What this item does instead

  • Every quote needs a full name and a link to where it was first published, so the claim can be checked, and readers can see the person said it in context.
  • Example quotes carry a visible "Example — replace with a real, permissioned quote" label, so a placeholder cannot ship looking like a real customer.
  • With no quotes, the section shows public records a reader can check (changelog, source code, status history) instead of inventing proof; with nothing at all, it renders nothing.
  • No logos, star ratings or counters, which are the parts most often invented and least often sourced.
  • The first quote leads and the rest follow in a quieter column, instead of identical cards that make every quote equally vague.
  • Links say where they go ("Read the case study"), and only links that leave the site get ↗.

Why it is built this way

Social proof is the section where generated UI most often makes things up: logos of companies that never used the product, round-number counters and five-star cards with first names. Each one is false the moment it ships, and placeholders like these are routinely left in.

This item only has room for proof a reader can check. A quote needs the person's full name, their role and a link to where they said it. Example quotes in the demo are marked with a visible label, and the component keeps that label for any quote passed with placeholder, so a forgotten example still says what it is.

When there is nothing to quote yet, pass an empty list and a fallback of public records: a changelog, the source code, a status history. They show what the team ships without claiming who uses it. If you have neither, the component renders nothing, which is better than an empty frame waiting for invented logos.

Keep permission records outside the UI. Before publishing a quote, get written consent for the exact words, the name and the role, and remove the quote when that consent is withdrawn.

Rules

How it breaks

Common mistakes when adapting this item.

  • Shipping the demo with the placeholder flag removed, so example quotes read as real customers.
  • Editing a quote to make it punchier, or trimming the parts that qualify it; publish the person's exact words.
  • Linking the source to a page that does not contain the quote, or to the home page.
  • Adding a logo strip or usage counters next to the quotes without a source and permission for each.
  • Keeping a quote after the person has left the organisation or asked for it to be removed.

States, responsive behaviour and accessibility

States
default, single, no proof yet
Desktop
From 1024px the first quote sits in a wider left column at a larger size and the others stack in a narrower right column, all sharing the heading's left edge; quote text keeps a readable measure.
Mobile
One column in reading order, heading first, then the lead quote and the others below it; the fallback links stack above their descriptions and stay at least 44px tall.

Accessibility

  • The section is labelled by its h2; each quote is a figure with a blockquote, its cite attribute set to the source, and a figcaption naming the person.
  • The example label is visible text above the quote, so placeholder status is not shown by the dashed border alone.
  • External links add "(another site)" for screen readers; the ↗ glyph is hidden from them.
  • Source and fallback links are at least 44px tall with a visible focus ring, and nothing moves or appears on hover only.

Code

web/registry/items/social-proof-honest/social-proof-honest.tsx

import * as React from "react";

import { cn } from "@/registry/shadcn/lib/utils";

export type ProofLink = {
  /** Where the link goes, in words: "Read the case study", "Read the original post". */
  label: string;
  href: string;
  /** True when the link leaves your site. Adds ↗ and "(another site)" for screen readers. */
  external?: boolean;
};

export type ProofQuote = {
  id: string;
  /** The person's exact words, unedited, without quotation marks. */
  quote: string;
  /** The real, full name of the person quoted. No initials or "Happy customer". */
  name: string;
  /** Their role and organisation, as they agreed to be named. */
  role?: string;
  /** Where the quote was first published, so readers can check it in context. */
  source: ProofLink;
  /** ISO date (YYYY-MM-DD) the quote was published. Shown so readers can judge how current it is. */
  publishedAt?: string;
  /**
   * Marks example content. Renders a visible label on the quote so a
   * placeholder can never ship looking like a real customer.
   */
  placeholder?: boolean;
};

export type ProofFallbackLink = ProofLink & {
  /** What a reader can check there, in one sentence. */
  description: string;
};

export type SocialProofHonestProps = Omit<
  React.ComponentProps<"section">,
  "title"
> & {
  title: React.ReactNode;
  description?: React.ReactNode;
  /**
   * One to three real quotes, each published with the person's permission.
   * The first is shown largest. Pass an empty array until you have one.
   */
  quotes: ProofQuote[];
  /**
   * Shown instead of quotes when there are none: public records a reader can
   * check (changelog, source code, status history). With no quotes and no
   * fallback the section renders nothing.
   */
  fallback?: ProofFallbackLink[];
  /** How the quotes were gathered and used (permission, edits, sources). */
  note?: React.ReactNode;
  /** Text on quotes marked `placeholder`. */
  placeholderLabel?: string;
  /** Screen-reader text added to external links. */
  externalLabel?: string;
  locale?: string;
};

const linkClass =
  "inline-flex min-h-11 items-center rounded-sm font-medium text-foreground underline underline-offset-4 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none";

function ProofAnchor({
  link,
  externalLabel,
}: {
  link: ProofLink;
  externalLabel: string;
}) {
  return (
    <a href={link.href} className={linkClass}>
      {/* One inline run, so a wrapped label keeps ↗ beside its last word. */}
      <span>
        {link.label}
        {link.external ? (
          <>
            <span aria-hidden="true">&nbsp;↗</span>
            <span className="sr-only"> ({externalLabel})</span>
          </>
        ) : null}
      </span>
    </a>
  );
}

function formatDate(iso: string, locale: string) {
  return new Intl.DateTimeFormat(locale, {
    year: "numeric",
    month: "long",
    timeZone: "UTC",
  }).format(new Date(`${iso}T00:00:00Z`));
}

function QuoteFigure({
  quote,
  featured,
  placeholderLabel,
  externalLabel,
  locale,
}: {
  quote: ProofQuote;
  featured: boolean;
  placeholderLabel: string;
  externalLabel: string;
  locale: string;
}) {
  return (
    <figure className="border-t border-border pt-6">
      {quote.placeholder ? (
        <p className="mb-4 inline-flex rounded-sm border border-dashed border-input px-2 py-1 text-sm font-medium text-foreground">
          {placeholderLabel}
        </p>
      ) : null}
      <blockquote
        cite={quote.source.href}
        className={cn(
          "text-pretty text-foreground",
          featured
            ? "max-w-[40ch] text-xl leading-snug font-medium sm:text-2xl"
            : "max-w-prose text-base leading-relaxed",
        )}
      >
        <p>“{quote.quote}”</p>
      </blockquote>
      <figcaption className="mt-5 text-sm leading-relaxed">
        <span className="block font-medium text-foreground">{quote.name}</span>
        {quote.role ? (
          <span className="block text-muted-foreground">{quote.role}</span>
        ) : null}
        <span className="mt-1 flex flex-wrap items-center gap-x-3 text-muted-foreground">
          <ProofAnchor link={quote.source} externalLabel={externalLabel} />
          {quote.publishedAt ? (
            <time dateTime={quote.publishedAt}>
              Published {formatDate(quote.publishedAt, locale)}
            </time>
          ) : null}
        </span>
      </figcaption>
    </figure>
  );
}

export function SocialProofHonest({
  title,
  description,
  quotes,
  fallback,
  note,
  placeholderLabel = "Example — replace with a real, permissioned quote",
  externalLabel = "another site",
  locale = "en-GB",
  className,
  ...props
}: SocialProofHonestProps) {
  const headingId = React.useId();
  const [featured, ...rest] = quotes;
  const hasFallback = !featured && fallback && fallback.length > 0;

  // No proof and nothing honest to show instead: render nothing rather than
  // an empty frame that invites placeholder logos.
  if (!featured && !hasFallback) return null;

  const figureProps = { placeholderLabel, externalLabel, locale };

  return (
    <section
      aria-labelledby={headingId}
      data-slot="social-proof-honest"
      className={cn("bg-background text-foreground", className)}
      {...props}
    >
      <div className="mx-auto w-full max-w-6xl px-4 py-12 sm:px-6 sm:py-16 lg:px-8 lg:py-24">
        <div className="max-w-2xl">
          <h2
            id={headingId}
            className="text-3xl leading-tight font-semibold tracking-tight text-balance sm:text-4xl"
          >
            {title}
          </h2>
          {description ? (
            <p className="mt-4 text-base leading-relaxed text-pretty text-muted-foreground">
              {description}
            </p>
          ) : null}
        </div>

        {featured ? (
          <div
            className={cn(
              "mt-10 grid gap-10",
              rest.length > 0 &&
                "lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)] lg:gap-16",
            )}
          >
            <QuoteFigure quote={featured} featured {...figureProps} />
            {rest.length > 0 ? (
              <ul className="grid content-start gap-8">
                {rest.map((quote) => (
                  <li key={quote.id}>
                    <QuoteFigure
                      quote={quote}
                      featured={false}
                      {...figureProps}
                    />
                  </li>
                ))}
              </ul>
            ) : null}
          </div>
        ) : null}

        {hasFallback ? (
          <ul className="mt-10 max-w-4xl border-t border-border">
            {fallback.map((link) => (
              <li
                key={link.href}
                className="grid gap-x-8 gap-y-1 border-b border-border py-4 md:grid-cols-[minmax(0,16rem)_minmax(0,1fr)] md:items-center"
              >
                <span className="text-base">
                  <ProofAnchor link={link} externalLabel={externalLabel} />
                </span>
                <p className="max-w-prose text-sm leading-relaxed text-muted-foreground">
                  {link.description}
                </p>
              </li>
            ))}
          </ul>
        ) : null}

        {note ? (
          <div className="mt-10 max-w-prose text-sm leading-relaxed text-muted-foreground">
            {note}
          </div>
        ) : null}
      </div>
    </section>
  );
}

Credits and licence

Contributors
None credited yet.
Provenance
Original work
Licence
MIT
Review
Community — not yet reviewed by a maintainer

For agents

The same taste notes and code as structured data.