2026-08-12 · Abdellahi

How to Add Testimonials to Any Website with Proofi

Proofi keeps testimonials in the dashboard and renders them on your site with a small script. You can change a quote or switch layouts without editing the page that contains the widget.

This guide starts with an empty account and ends with a working embed. It also covers the JSON endpoint and the first things to check when nothing appears.

How Proofi fits together

A review holds the quote and reviewer details. It can also include a rating, photo, and display date. Widget settings decide how those reviews look and which fields appear. The embed ties a target element on your page to one widget ID.

There is no separate Publish step. The editor saves changes as you make them, and the widget becomes active after you add its first review. Public responses are cached, so visitors may not see an edit immediately.

1. Create or open a widget

Create a Proofi account and open the dashboard. A new account already has a widget called My First Widget, and Proofi opens its editor. To make another one, return to the dashboard and click New Widget.

You can rename a widget by clicking its name in the editor header. Names such as Homepage proof and Pricing page reviews are easier to recognize later. The name never becomes the HTML element ID.

Beside the name, you will see the first few characters of the widget's UUID. Click that short value to copy the full ID for a custom integration.

2. Add the testimonials

Click Add Review in the left panel. The form only requires a reviewer name and the review text. Ratings start at five stars. A profile photo, role, company, and display date are optional.

The display date is a text field rather than a date picker. That leaves room for August 2026, 2 weeks ago, or Verified customer, depending on how the original review presents its date or status.

One review is enough for a carousel. I would add several before judging a grid or bento layout, since their structure is hard to see with a single card.

Drag reviews in the left panel to set their order. Proofi saves the new order immediately. The same panel has actions to edit, duplicate, select, and delete reviews.

Only add content you have permission to publish. Widget data is public and can remain in caches for a while after an edit or deletion.

3. Choose a layout

The layout controls are under the preview. Carousel shows one review at a time and works well in narrow spaces. Grid uses two, three, or four columns on wider screens, while List gives longer testimonials a straightforward reading order.

The other layouts are more specialized. Bento builds an asymmetric grid. Spotlight sets one review beside a supporting list. Marquee moves cards horizontally, and Minimal strips away most card decoration. Badge condenses the reviews into a rating summary.

Judge the layout at the width it will receive on the real page. A four-column grid can look fine in a full-width preview and feel crowded in a 500-pixel sidebar.

The preview runs the same compiled widget as the public embed. It reloads its preview document when the reviews change.

4. Adjust the presentation

Each settings change saves as you make it. The bar below the preview has the light and dark theme switch, accent color, border radius, and controls for stars and reviewer photos.

The three-dot menu contains the rest. You can hide dates, roles, quote marks, or company names. It also has shadow and font choices, the review limit, and options for the current layout.

Grid and bento support two to four columns. Carousel and marquee can play automatically at intervals from two to ten seconds. Carousel also has separate controls for navigation buttons and dots. You can show between one and twenty reviews.

Proofi renders inside Shadow DOM, so broad rules on the host page, such as button { border: 0 } or p { margin: 2rem }, do not restyle its cards. Use the Proofi editor for visual changes. If you need to own the markup as well as the CSS, use the JSON endpoint later in this guide.

5. Add the embed to your site

Click Embed in the editor header and open the Embed Code tab. Proofi generates this pair of elements:

HTML
<div id="jts-widget-YOUR_WIDGET_ID"></div>
<script
  src="https://proofi.co/widget.js"
  data-widget-id="YOUR_WIDGET_ID"
  async
></script>

Paste both elements where the testimonials belong. Replace each YOUR_WIDGET_ID with the same UUID. If the target ID and data-widget-id differ, the loader will not find its mount point.

Keep the target element above the script. The loader checks for that target once, as soon as the script runs. This ordering is easy to break by moving the script into the document head or loading it through a tag manager.

In a site builder, put the snippet in a custom HTML or embed block. In a server-rendered application, put it in the template that owns the testimonial section. Because the script is asynchronous, its download does not block the HTML parser.

For a widget above the fold, reserve some space before it loads. That will reduce layout shift:

HTML
<div class="customer-proof">
  <div id="jts-widget-YOUR_WIDGET_ID"></div>
  <script
    src="https://proofi.co/widget.js"
    data-widget-id="YOUR_WIDGET_ID"
    async
  ></script>
</div>
CSS
.customer-proof {
  min-height: 280px;
}

280px is only a starting point. Measure the chosen layout at its actual page width and adjust the value.

Mount Proofi in React or Next.js

For a client-rendered page, create the script after the component mounts. The loader will then run after client-side navigation, when its target is already in the document:

TSX
'use client'
 
import { useEffect } from 'react'
 
export function ProofiWidget({ widgetId }: { widgetId: string }) {
  useEffect(() => {
    const script = document.createElement('script')
    script.src = 'https://proofi.co/widget.js'
    script.dataset.widgetId = widgetId
    script.async = true
    document.body.appendChild(script)
 
    return () => script.remove()
  }, [widgetId])
 
  return <div id={`jts-widget-${widgetId}`} />
}

Pass the component the ID copied from the Proofi editor:

TSX
<ProofiWidget widgetId="YOUR_WIDGET_ID" />

The effect runs after React commits the target element, which gives the loader something to find. Every mounted widget needs its own target and one execution of the script. They all use the same widget.js URL, which the browser may reuse according to its cache headers.

What the browser does with the snippet

The loader reads data-widget-id, looks for #jts-widget-{id}, and attaches a Shadow Root. It mounts the Proofi runtime there and makes this request:

Text
GET https://proofi.co/api/widget?id=YOUR_WIDGET_ID

The response contains the settings and ordered reviews. The runtime applies the Max Reviews limit, picks the configured layout, and renders inside the Shadow Root. An empty review list produces no output.

The widget makes one request for its data. Unrestricted widgets return browser and shared-cache headers. The account and plan determine how long that payload can stay cached.

Render the JSON yourself

The API tab in the Embed dialog shows the same endpoint. It is useful when you want to manage the content in Proofi but render your own HTML and CSS.

TypeScript
type ProofiReview = {
  id: string
  reviewer_name: string
  reviewer_role: string | null
  reviewer_company: string | null
  rating: number | null
  content: string
  avatar_url: string | null
  display_date: string | null
}
 
type ProofiPayload = {
  widget: {
    id: string
    name: string
    settings: Record<string, unknown>
  }
  reviews: ProofiReview[]
  showBranding: boolean
  brandingStyle: string
}
 
export async function getProofiWidget(widgetId: string) {
  const url = new URL('https://proofi.co/api/widget')
  url.searchParams.set('id', widgetId)
 
  const response = await fetch(url)
  if (!response.ok) {
    throw new Error(`Proofi request failed: ${response.status}`)
  }
 
  return response.json() as Promise<ProofiPayload>
}

Do not store private notes or unpublished customer data in a widget. Visitors need public access to this endpoint for the standard embed to work. A custom renderer should also honor the account's showBranding value.

When the widget is blank

Start in the browser's Network panel. A normal first load has two relevant responses:

  1. widget.js returns 200.
  2. api/widget?id=... returns 200 with at least one item in reviews.

The API status usually points to the problem:

  • 400 means the ID is missing or is not a valid UUID.
  • 403 means the current site is outside the widget's allowed domains.
  • 404 means the widget could not be found.

If both requests return 200, inspect the target element. Its ID should match the script attribute, and the element should contain a #shadow-root. When the API succeeds with an empty reviews array, add a review in the editor.

If the dashboard preview has the edit but the public page does not, inspect the API response. An older payload there means the cache is still active. Wait for that cache window rather than changing the embed code; the widget ID and script URL stay the same when its content changes.

Create your first Proofi widget