Blog//7 min read
Review marquee and testimonials wall: showing Google reviews on your site
How to show Google reviews on a website in React: static JSON or the Places API, a marquee for density, a wall for depth, and what review schema allows.
MSMarco Schulz, founder of Facade UI
Every local business client asks the same thing once the site is live: "Can we show our Google reviews?" They have earned a 4.9 with a few hundred reviews and want it on the home page. The request sounds simple and has three problems inside it: where the review text comes from, how to lay it out so people read it, and what you are allowed to put in structured data. I built the Review marquee and the Testimonials wall in Facade to solve the layout half. This article covers all three.
Where the review data comes from
You have two realistic sources, and a third that looks tempting and is not.
Static JSON, curated by hand
The business owner picks fifteen reviews they are proud of, you paste them into a JSON file with first name, city, rating, text, source and date, and the site renders them. It costs nothing, never breaks, and loads with the page. It does not update itself, and in practice that is fine. A cleaning company does not need yesterday's review on the home page; it needs its twelve strongest, and those change twice a year. This is what I do on most sites.
The Google Places API
The Places API can return reviews for a place ID. Two things to know before you build on it. It returns a limited set of reviews per place, not the full history, so you cannot pull "all 340 reviews" from it. And it comes with usage terms about how long you may cache the data and how you must attribute it to Google. Read the current terms before you ship. If you use it, fetch on the server, cache briefly, render from the cache, and never call it from the browser with an exposed key.
Scraping the Maps page
Do not. It breaks every few months, it violates the terms, and the client's listing is the one on the line.
The data shape
Whatever the source, normalize into one shape the components understand. The marquee takes a Review:
export interface Review {
name: string;
city?: string;
rating: number;
text: string;
source?: string;
date?: string;
}The wall takes a Testimonial with a typed source of "google" | "yelp" | "facebook", an optional service tag and a quote instead of text. Both are plain arrays, so a small mapping function turns a Places response or a JSON file into either.
The marquee: density
The Review marquee is for the moment a visitor needs to see that there are a lot of reviews and they are all good. Two rows of cards scroll in opposite directions, the top row left, the bottom row in reverse, with soft faded edges on both sides so cards appear and disappear instead of being cut off. Each card shows the stars, the text, the name, and the city and date on one line, plus a small "via Google" label when a source is set.
The scroll pauses on hover by default, so a visitor who spots a review can read it. The track is duplicated for a seamless loop, and the duplicate is marked aria-hidden so screen readers hear each review once. duration sets the seconds per loop and defaults to 60; slower is better than you think.
import { ReviewMarquee, type Review } from "@/components/facade/review-marquee";
import reviews from "@/data/reviews.json";
<ReviewMarquee reviews={reviews as Review[]} rows={2} duration={48} pauseOnHover />Use the marquee high on the page, under the hero or the stats band, where it works as proof rather than as reading material.
The marquee is a Pro component. It installs from the private registry once the license key is in components.json (the Pro setup takes a minute):
npx shadcn@latest add @facade/review-marqueeThe wall: depth
The Testimonials wall is for the moment a visitor wants to read. It opens with an aggregate header: the rating as a large number, stars, and the review count formatted with a thousands separator. Under it, a featured quote with the reviewer's name and role, then a masonry wall of cards in one, two or three CSS columns depending on the screen, each card with stars, the quote, the name, location, date, the source label and an optional service tag.
The cards fade and rise into view as the visitor scrolls, with a small stagger across each row of three so the wall settles instead of popping. Masonry here is columns-3 with break-inside-avoid, no JavaScript layout, which means the wall renders correctly on the server and never jumps after hydration.
import { TestimonialsWall, type Testimonial } from "@/components/facade/testimonials-wall";
const testimonials: Testimonial[] = [
{ name: "Dana R.", location: "Lakewood", date: "Oct 2026", rating: 5, source: "google", service: "Deep clean", quote: "Same two cleaners every visit, and they remember that the baby naps at one." },
{ name: "Marcus T.", location: "Edgewater", date: "Sep 2026", rating: 5, source: "yelp", service: "Move-out clean", quote: "Got the full deposit back. The landlord asked who we used." },
{ name: "Priya S.", location: "Wheat Ridge", date: "Sep 2026", rating: 4, source: "google", quote: "One reschedule in eight months, and they told us a day ahead." },
];
<TestimonialsWall
eyebrow="Reviews"
heading="What our clients say"
rating={4.9}
reviewCount={312}
featured={{ name: "The Alvarez family", role: "Weekly clients since 2023", quote: "We stopped comparing cleaners after the first month.", rating: 5 }}
testimonials={testimonials}
ctaLabel="Read all reviews on Google"
ctaHref="https://maps.google.com/?cid=YOUR_PLACE_CID"
/>Keep a four star review in the wall. A page of nothing but five stars reads as filtered; one honest four with a specific detail makes the rest believable.
npx shadcn@latest add @facade/testimonials-wallAggregate rating schema: what is allowed
This is the part that gets agencies in trouble. It is tempting to wrap the wall in LocalBusiness schema with an aggregateRating of 4.9 and 312 reviews, hoping for stars in search results.
Google's structured data documentation says that reviews and ratings about a business, published on that business's own site, are considered self-serving and are not eligible for review rich results. In practice that means the stars in the search snippet are not going to come from your client's home page, no matter how correct the markup is. The same guidelines say ratings must be collected directly from users on the site, not sourced from other sites.
So the Facade wall does not emit AggregateRating or Review JSON-LD, on purpose. What I do instead:
- Point the wall's
ctaHrefat the Google Business Profile, so the visitor can verify the rating and count in one click. - Keep the
LocalBusinessschema on the page for the name, address, phone and hours, without a rating. - Let the Google Business Profile carry the rating in search. That is where Google shows it anyway.
If a client insists on rating markup, show them the documentation and let them decide. A manual action against the site costs more than the stars were ever going to earn.
Which one, or both
On a cleaning service site I use both: the marquee under the stats band for proof at a glance, the wall after the pricing for the visitor who is about to book. On a smaller page, pick the wall. Six real reviews with names and neighborhoods do more than sixteen that scroll past.
FAQ
Can I show the Google logo next to the reviews?
Follow Google's brand guidelines for the logo itself. The components render a plain text "via Google" or a "Google" source label, which is attribution without a trademark question. If you pull from the Places API, the attribution requirements in the terms apply regardless of how you lay the reviews out.
Should I show reviewer full names?
I show first name and last initial, plus a neighborhood, and I ask the business to confirm each one. Reviewers wrote publicly, but moving their words to a site they did not choose is worth a sentence of courtesy, and a first name is enough for credibility.
How often should the reviews change?
Every quarter is plenty for a curated JSON file. Swap out the oldest two, add the two strongest recent ones, keep the mix of services. If you use the Places API with a short cache, the marquee stays fresh on its own, but curate the wall by hand regardless.