svelte-google-reviews
v2.0.0
Published
Svelte component for displaying Google Reviews
Downloads
1,093
Readme
Svelte Google Reviews
Drop Google Reviews into any Svelte 5 or SvelteKit app. Ships three ready-made layouts (badge, carousel, custom slot), pulls data from either the free Featurable API or the Google Places API, and works out of the box with SSR.
📖 Live docs & interactive examples →
Table of contents
- Features
- Installation
- Quickstart
- SvelteKit + SSR
- Layouts
- Props
- CSS customization
GoogleReviewtype- Troubleshooting
- Differences from
react-google-reviews - License
Features
- 🛠️ Customizable: Three layout options with extensive CSS class/style prop overrides
- 🔎 SEO-friendly: JSON-LD structured data for search engines
- 💻 Responsive: Works on all devices and screen sizes
- ⚡ Fast: Lightweight — no React, no Emotion, no jQuery
- ✨ Free: Use the Featurable API at no cost
- 🌱 Fresh: Auto-updates reviews every 48 hours via the Featurable API
- ♿️ Accessible: WAI-ARIA compliant carousel and interactive elements
- 🪶 Lightweight: Scoped CSS, minimal dependencies (embla-carousel only)
Installation
npm install svelte-google-reviewsyarn add svelte-google-reviewspnpm add svelte-google-reviewsUsage
Using the Featurable API (recommended)
Prerequisites:
- Create a free account at https://featurable.com
- Create a new Featurable widget
- Click Embed > API and copy the widget ID
<script>
import { SvelteGoogleReviews } from 'svelte-google-reviews';
// Create a free widget at https://featurable.com
const featurableWidgetId = '842ncdd8-0f40-438d-9c...'; // use "example" for testing
</script>
<SvelteGoogleReviews layout="carousel" featurableId={featurableWidgetId} />Note: The Featurable API is free and provides caching, automatic updates, and unlimited reviews.
Using static reviews (Google Places API or manual)
The Google Places API is limited to the 5 most recent reviews. Fetch them server-side and pass them directly:
<script lang="ts">
import { SvelteGoogleReviews } from 'svelte-google-reviews';
import type { GoogleReview } from 'svelte-google-reviews';
// Fetched server-side via Google Places API
let { reviews }: { reviews: GoogleReview[] } = $props();
</script>
<SvelteGoogleReviews layout="badge" {reviews} isLoading={false} />Note: Never expose your Google API key client-side. Always fetch reviews server-side (e.g. in a SvelteKit
+page.server.tsload function).
SvelteKit + SSR
The component is SSR-safe: it renders a placeholder on the server and hydrates on the client. The carousel (embla) only initializes in the browser, so there is no window is not defined risk.
Fetching from Google Places API (server-side)
Create a +page.server.ts next to the page that renders <SvelteGoogleReviews />:
// src/routes/+page.server.ts
import { GOOGLE_PLACES_API_KEY, GOOGLE_PLACE_ID } from '$env/static/private';
import type { PageServerLoad } from './$types';
import type { GoogleReview } from 'svelte-google-reviews';
export const load: PageServerLoad = async ({ fetch }) => {
const url = `https://places.googleapis.com/v1/places/${GOOGLE_PLACE_ID}?fields=reviews,rating,userRatingCount&key=${GOOGLE_PLACES_API_KEY}`;
const res = await fetch(url);
const data = await res.json();
const reviews: GoogleReview[] = (data.reviews ?? []).map((r: any) => ({
reviewId: r.name ?? null,
reviewer: {
displayName: r.authorAttribution?.displayName ?? 'Anonymous',
profilePhotoUrl: r.authorAttribution?.photoUri ?? '',
isAnonymous: false,
},
starRating: r.rating,
comment: r.originalText?.text ?? r.text?.text ?? '',
createTime: r.publishTime ?? null,
updateTime: r.publishTime ?? null,
}));
return { reviews, averageRating: data.rating, totalReviewCount: data.userRatingCount };
};Then consume it in the page:
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { SvelteGoogleReviews } from 'svelte-google-reviews';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<SvelteGoogleReviews
layout="carousel"
reviews={data.reviews}
isLoading={false}
structuredData
averageRatingOverride={data.averageRating}
totalReviewCountOverride={data.totalReviewCount}
/>The
$env/static/privateimport ensures your API key never ships to the client. Cache the load withsetHeaders({ 'cache-control': 'public, max-age=3600' })to avoid hitting the Places quota on every request.
Non-SvelteKit environments
For Vite / Astro / plain Svelte apps, wrap the component in a client-only boundary if your SSR framework doesn't natively support hydration, and fetch reviews at build time or from your own backend.
Layouts
Badge
Displays a compact badge with the average rating, star visualization, and a link to your Google profile.
<SvelteGoogleReviews layout="badge" featurableId={featurableWidgetId} />Carousel
An interactive, autoplay carousel showing individual review cards.
<SvelteGoogleReviews layout="carousel" featurableId={featurableWidgetId} />Custom
Provides full control via a children snippet. The reviews array is passed to the snippet.
<SvelteGoogleReviews layout="custom" featurableId={featurableWidgetId}>
{#snippet children({ reviews })}
{#each reviews as review}
<div>
<strong>{review.reviewer.displayName}</strong>
<p>{review.comment}</p>
</div>
{/each}
{/snippet}
</SvelteGoogleReviews>Migration note from React: The React
rendererprop is replaced by Svelte 5'schildrensnippet pattern.
Props
Common Props
| Prop | Type | Default | Description |
| -------------------------- | ----------------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------- |
| featurableId | string | — | Featurable widget ID |
| reviews | GoogleReview[] | — | Static reviews array (mutually exclusive with featurableId) |
| isLoading | boolean | — | Controls loading state when passing reviews manually |
| layout | "carousel" \| "badge" \| "custom" | "carousel" | Layout variant |
| widgetVersion | "v1" \| "v2" | "v1" | Featurable API version |
| apiBaseUrl | string | "https://api.featurable.com" | Custom API base URL |
| nameDisplay | "fullNames" \| "firstAndLastInitials" \| "firstNamesOnly" | "firstAndLastInitials" | How reviewer names are shown |
| logoVariant | "icon" \| "full" \| "none" | "icon" | Google logo style |
| maxCharacters | number | 200 | Max characters before "Read more" truncation |
| dateDisplay | "relative" \| "absolute" \| "none" | "relative" | How review dates are shown |
| reviewVariant | "card" \| "testimonial" | "card" | Card layout variant |
| theme | "light" \| "dark" | "light" | Color scheme |
| hideEmptyReviews | boolean | false | Hide reviews without text |
| disableTranslation | boolean | false | Use original (untranslated) review text |
| accessibility | boolean | true | Enable ARIA roles and focus management |
| structuredData | boolean | false | Inject JSON-LD structured data for SEO |
| brandName | string | page title | Brand name for structured data |
| productName | string | page title | Product name for structured data |
| productDescription | string | "" | Product description for structured data |
| totalReviewCountOverride | number | — | Required for structured data when passing reviews manually |
| averageRatingOverride | number | — | Required for structured data when passing reviews manually |
| errorMessage | string | "Failed to load Google reviews..." | Custom error text |
| loadingMessage | string | "Loading reviews..." | Custom loading text |
Carousel Props
| Prop | Type | Default | Description |
| ------------------ | ------------------------ | -------------------- | ------------------------------------ |
| carouselSpeed | number | 3000 | Autoplay interval in ms |
| carouselAutoplay | boolean | true | Enable autoplay |
| maxItems | number | 3 | Max slides visible at once (desktop) |
| readMoreLabel | string | "Read more" | Expand label for truncated reviews |
| readLessLabel | string | "Read less" | Collapse label |
| showDots | boolean | true | Show navigation dots |
| getRelativeDate | (date: Date) => string | built-in | Custom relative date formatter |
| getAbsoluteDate | (date: Date) => string | toLocaleDateString | Custom absolute date formatter |
Badge Props
| Prop | Type | Default | Description |
| -------------------------- | ----------------------- | ---------------------- | --------------------------- |
| profileUrl | string | auto-fetched | Google Business profile URL |
| badgeLabel | string | "Google Rating" | Badge heading |
| badgeSubheadingFormatter | (n: number) => string | "Read our N reviews" | Custom subheading |
CSS Customization
Every element exposes a *ClassName and *Style prop for targeted overrides. Style props accept inline style strings (e.g. "color: red; font-size: 14px").
<SvelteGoogleReviews
layout="carousel"
featurableId="example"
reviewCardLightClassName="my-card"
reviewReadMoreLightStyle="color: blue;"
/>See the live Storybook docs for a full interactive reference of every prop.
GoogleReview Type
type GoogleReview = {
reviewId: string | null;
reviewer: {
profilePhotoUrl: string;
displayName: string;
isAnonymous: boolean;
};
starRating: number; // 1–5
comment: string;
createTime: string | null;
updateTime: string | null;
};Storybook
Interactive docs are hosted at ohmybugs.github.io/svelte-google-reviews — every prop, layout, and theme variant has a live example.
Run locally:
npm run storybookTroubleshooting
Reviews render but styles are missing
Unlike the React version, this package uses Svelte's scoped <style> blocks — no CSS import is required. If styles are missing, check that your bundler processes .svelte files from node_modules (SvelteKit and @sveltejs/vite-plugin-svelte do this by default; custom Vite configs may need include: ['svelte-google-reviews'] in the Svelte plugin options).
window is not defined during SSR
Shouldn't happen — the carousel initializes in onMount. If you see it, you're likely rendering the component before your framework's hydration boundary. In SvelteKit, ensure you're not calling it inside a +page.server.ts file.
Google Places API returns only 5 reviews
That is a hard limit of the Places API. Use the Featurable API (free, includes caching) if you need all reviews.
Reviews look stale
- Featurable API: refreshes every 48 hours by default.
- Google Places API: cache your
+page.server.tsload withsetHeaders({ 'cache-control': 'public, max-age=3600' })to avoid rate limits — but don't cache longer than a few hours or new reviews won't appear.
Hydration mismatch warning in the console
Make sure the reviews prop is identical between server and client renders. If you compute anything time-based (e.g. relative dates) at the top level, pass a stable value or override getRelativeDate with a deterministic formatter.
TypeScript can't find GoogleReview type
Import types explicitly:
import type { GoogleReview } from 'svelte-google-reviews';If you see Cannot find module 'svelte-google-reviews' or its corresponding type declarations, ensure your tsconfig.json uses "moduleResolution": "bundler" or "NodeNext".
Differences from react-google-reviews
| Feature | React | Svelte |
| --------------------- | --------------------------------- | ----------------------- |
| Custom layout | renderer prop (render function) | children snippet |
| Style props | React.CSSProperties object | inline style string |
| Error/loading content | React.ReactNode | string |
| Carousel library | react-slick | embla-carousel-svelte |
| CSS | Emotion CSS-in-JS | Scoped <style> blocks |
| CSS import required | Yes (dist/index.css) | No |
License
MIT — see LICENSE. By using the Featurable API you agree to the Featurable Terms of Service.
Acknowledgements
- react-google-reviews by Featurable — the original library this is adapted from
- embla-carousel — carousel engine (MIT)
