Other
Mocha for agencies and developers: embedding on client websites
This page is for agencies and developers who add Mocha to their clients' websites. Practices, law firms and consultancies can simply forward the link to their agency.
TL;DR in 5 steps
- Have the client create the Mocha account, with one space per client or location.
- In the space, copy the embed code under “Widget” with “Copy code”.
- Place the script tag exactly where the widget should appear in the content.
- In single-page apps, insert the script into a container with JavaScript after mount.
- Allow the Mocha domain in your CSP and exclude the script from JS optimisation.
Accounts and spaces
Mocha has no team or multi-user accounts. An account has one login (email and password, or Google), and every space belongs to exactly one account. There is currently no way to move a space to another account.
We therefore recommend that your client creates the account and subscribes to the plan themselves. The reviewers' consent, the consent log and the invoices then sit with the client and stay there if they change agency. You do not need a login to embed anything: the script tag is public, so your client can simply send you the copied code.
If you run your own account with one space per client instead, the reviews and consent records sit in your account. Check with your data protection adviser which agreements you need with your clients for that.
Create one space per client or location (Create a space). Each space has its own form, its own widgets and its own Google link on the thank-you page. The free plan includes one space and shows up to 50 approved reviews with a “Powered by Mocha” note. Pro costs €19 per month (14-day free trial) and adds unlimited spaces and reviews, your own logo and colours, and no Mocha note (Pricing).
The embed code
The dashboard generates the code: open the space, choose Widget under Embed in the space navigation, pick a widget under Choose a widget, adjust it under Appearance and click Copy code.
<script
src="https://mocha.de/api/embed/your-space/widget.js"
data-mode="wall"
async></script>How the script behaves:
- Placement: the script finds its own tag via
document.currentScriptand inserts a<div data-mocha-widget="your-space">directly after it. The widget appears exactly where the script tag sits. - Shadow DOM: the widget renders inside an open shadow root. Your page's CSS does not leak in and the widget's CSS does not leak out. Inherited properties such as the font family come from your page. Control width and outer spacing with the
[data-mocha-widget]selector. - Asynchronous: with
async, the script does not block page rendering. Until the data has loaded the widget has no height, so a minimum height on the surrounding container reduces layout shifts. - Live data: the widget fetches the approved reviews from Mocha on every page view. That response may be cached for 60 seconds and the script itself for an hour, so newly approved reviews appear without any change to the code.
- Empty rather than an error: if the space is deactivated or has no approved review yet, the widget stays empty.
- Several widgets: each script tag creates its own widget, including several on one page.
Data attributes
The dashboard only writes values into the code that differ from the default. If an attribute is missing, the default from the table applies.
| Attribute | Values | Default | Applies to |
|---|---|---|---|
data-mode | slider, wall, single, badge and others from “Choose a widget” | slider | all |
data-theme | light, dark | light | all |
data-radius | corner radius in pixels, 0, 12 or 24 in the dashboard | 12 | all with cards |
data-shadow | none, subtle, strong | subtle | all with cards |
data-font-size | sm, md, lg | md | all |
data-autoplay | true, false | slider: on, portrait: off | slider, portrait |
data-speed | milliseconds, 3000 to 8000 in the dashboard | 4000 (portrait: 3000) | slider, portrait |
data-arrows | false hides them | on | slider, portrait |
data-dots | false hides them | on | slider |
data-columns | number of columns, 2 to 4 in the dashboard | 3 | wall, masonry |
data-gap | gap in pixels, 8, 16 or 24 in the dashboard | 16 | wall, masonry |
data-align | left, center | left | single |
data-badge-style | pill, card, minimal | pill | badge |
data-stars | false hides the star | on | badge |
The dashboard adds further options for individual widgets, such as scrolling speed or card width, to the code when needed. The accent colour for stars and highlights comes from the space, not from the code. The column count applies at every screen width, so the slider is a better fit for narrow areas.
Single-page apps: React, Next.js, Vue
A script tag in a template usually does not work as intended in a single-page app:
- Browsers do not run scripts inserted via
innerHTMLordangerouslySetInnerHTML. - React 19 moves
<script async src="…">from JSX into the<head>and removes duplicate scripts with the same URL (React documentation). The widget would then end up in the<head>. next/scriptappends the script to the end of<body>, not where the component is.- Vue ignores
<script>tags in client component templates.
Instead, insert the script element into a container with JavaScript after mount, and empty the container on unmount:
"use client";
import { useEffect, useRef } from "react";
export function MochaWidget({ slug, mode = "wall" }: { slug: string; mode?: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = ref.current;
if (!container) return;
const script = document.createElement("script");
script.src = `https://mocha.de/api/embed/${slug}/widget.js`;
script.async = true;
script.dataset.mode = mode; // more, e.g. dataset.theme = "dark", dataset.fontSize = "lg"
container.appendChild(script);
return () => container.replaceChildren();
}, [slug, mode]);
return <div ref={ref} style={{ minHeight: 320 }} />;
}The script still finds itself via document.currentScript and places the widget inside the container. In Vue the approach is the same: insert the element in onMounted and empty the container in onBeforeUnmount. Every insertion fetches the data again and counts as a view in the widget statistics. Alternatively, embed the review wall as an iframe (see below).
Tag managers
We do not recommend a tag manager for widgets in the page content. The widget appears wherever the script tag ends up in the DOM, and Google Tag Manager adds custom HTML tags at the end of <body>, which would put the widget below the footer.
If it has to be a tag manager, add a container with a fixed ID to the page template and have the tag insert the script there with JavaScript:
<!-- In the page template, where the widget should appear -->
<div id="mocha-reviews"></div>
<!-- Custom HTML tag in the tag manager -->
<script>
(function () {
var target = document.getElementById("mocha-reviews");
if (!target) return;
var s = document.createElement("script");
s.src = "https://mocha.de/api/embed/your-space/widget.js";
s.setAttribute("data-mode", "wall");
s.async = true;
target.appendChild(s);
})();
</script>Content Security Policy
If the website has a Content Security Policy, the widget needs these sources:
| Directive | Source | Used for |
|---|---|---|
script-src | https://mocha.de | widget.js and the video player |
connect-src | https://mocha.de | loading reviews, sending events for the widget statistics |
img-src | hosts of the review images | photos and video thumbnails; for imported Google reviews, profile photos from Google |
style-src | 'unsafe-inline' | the <style> element in the Shadow DOM and style attributes |
media-src | host of the videos | video player only |
frame-src | https://mocha.de | iframe embeds only |
A nonce does not help with style-src, because the widget creates its <style> element itself. Without this source the widget appears unstyled. You can see the exact image and video hosts in the Network tab of the developer tools. The widget does not work with require-trusted-types-for 'script', because it uses innerHTML.
Caching and optimisation plugins
Exclude widget.js from every feature that combines, moves or delays JavaScript until the first interaction. The script needs its own tag in place: it reads the space and options from it and uses it to position the widget. Copied into a bundle, it cannot find its tag and stays empty. Notes on common plugins are on the WordPress page.
Other ways to embed
Review wall as an iframe: below the embed code, iFrame embed opens an <iframe> code for https://mocha.de/wall/your-space. The page accepts the parameters theme (light, dark), columns (2, 3, 4) and limit (6, 12, 24, default 12). The iframe does not resize to its content, so set height to suit. This works for systems that do not allow scripts in the content.
A single video: for an approved video review, open Testimonials, open the review's details (View details) and find the code in the Embed this video section. The player also renders in a Shadow DOM directly after its script tag, so the same rules for SPAs apply.
<script
src="https://mocha.de/api/embed/video/VIDEO-ID/player.js"
data-id="VIDEO-ID"
async></script>Form and QR code: the form lives at https://mocha.de/s/your-space; copy the link with Copy link in the space navigation. Create the matching QR code under Embed in the QR code section.
Privacy on the client's website
These points describe what happens technically. They are not legal advice.
- Visitors' browsers load the script and the review data from
mocha.de, and photos and videos from the media servers Mocha uses for them. As with any request, these servers receive the IP address. Profile photos of imported Google reviews come from Google. - When the widget is shown it sends a “view” event to Mocha, and a “click” event when someone clicks a review card. Each event contains the space, the widget style, the review where relevant, and the full page address including URL parameters. Mocha also stores the browser's user agent, but not the IP address, and deletes individual events after 13 months. This is what the space's widget statistics are built from.
- With
data-track="false"on the script tag the widget sends no events, and the statistics stay empty for that embed. - The widget sets no cookies and does not use local storage in the browser.
Mocha's privacy policy (in German) describes how Mocha processes this data. Whether and how the client's own privacy policy should mention the widget under the GDPR is a question for the client and their data protection adviser.