Install the widget
Add Pinwren to any website with one script tag, from npm, or as a React component.
The widget is a single script, widget.js, served by your Pinwren server. It renders inside a closed Shadow
DOM attached to its own <pinwren-root> element, so it can't be styled or broken by your page's CSS, and it
never throws into your code. The core is under 35 KB gzip (about 27 KB with brotli); the side panel, bug report form, screenshot engine,
annotation editor and each language load only when a visitor needs them.
Before you start
Create a project in the dashboard and add your site under Allowed sites on its Install tab (for
example example.com, *.example.com or localhost:5173). The widget identifies itself with the project's
public key (pk_…) and the server only answers requests from those origins — no secret ever reaches the
browser.
Paste the snippet from the project's Install tab before </body> (the tab also has Next.js and Google
Tag Manager versions):
<script src="https://feedback.example.com/widget.js" data-project="pk_…" async></script>That's all: the widget starts once the page has loaded, fetches the project's configuration and shows the launcher.
Check the installation
On the project's Install tab, Check installation fetches each of your Allowed sites from the server and looks for the widget's script tag. For every site it says one of:
- Widget found: the tag is on the page with this project's key.
- Script tag not found: paste the snippet into the page's
<head>or your tag manager, publish and check again. Pages rendered only in the browser cannot be read this way; open the site and look for the launcher. - Another project's key is on the site: the page uses a different
data-projectthan this project's. - Couldn't reach the site, with the reason: timeout, host name does not resolve, invalid TLS certificate, page too large, an error status, or a private or reserved address (the server will not contact those; use the terminal check on the tab).
Below the result the tab also shows whether the widget has ever loaded from any site, when it last did, and whether an origin was refused because it is not under Allowed sites.
Script attributes
| Attribute | Required | What it does |
|---|---|---|
data-project | Yes | The project's public key (pk_…). Without it the script loads but does nothing, so you can start the widget yourself with Pinwren.init(). |
data-endpoint | No | Your Pinwren server's origin, e.g. https://feedback.example.com. Defaults to the origin the script was loaded from. |
data-accent | No | Launcher colour as a hex value (#d63a1c). Overrides the project's accent on this page only; anything that isn't a plain hex colour is ignored. |
data-locale | No | UI language, e.g. fr or pt-BR. Otherwise the visitor's browser languages decide — see Languages. |
data-force-show | No | Show the widget even where the project's visibility rules would hide it. |
data-url-pattern | No | Group this page's pins with every page that matches a pattern such as /product/:id — see Visibility & page scope. |
data-nonce | No | A CSP nonce the widget copies onto the chunk and locale scripts it adds (the script's own nonce works too). |
data-allow-replay-in-privacy-mode | No | Keep session replay on for a project in privacy mode; you take care of consent. |
data-release | No | The version of your site (1.4.2). Bug reports carry it as metadata.release, so the dashboard shows which release they came from and flags regressions with more confidence. |
data-theme | No | light or dark to force the widget's theme; otherwise it follows the visitor's system setting. |
Content Security Policy
If your site sends a Content-Security-Policy, allow your Pinwren server's origin for scripts (the widget
loads its chunks and languages from the folder widget.js came from), for connect-src (the API calls) and
for img-src (a logo in the report dialog, when the project has one). The widget uses no eval and no
inline scripts; with a nonce-based policy, put the nonce on the script tag and the widget copies it onto
every script it adds. The screen recording preview plays a blob: video, so a strict policy
needs media-src blob: as well.
Content-Security-Policy: script-src 'self' https://feedback.example.com; connect-src 'self' https://feedback.example.com; img-src 'self' data: https://feedback.example.comWhat happens on load
- The script reads its attributes and waits for
DOMContentLoadedif the page is still loading. - It fetches
GET /w/configwith the project key: the project's launcher, modes, capture and privacy settings (Configuration). If the key is unknown or the origin isn't allowed, the widget logs[pinwren] widget disabled: …to the console and removes itself. If the project's visibility rules hide it on this host, it stops here too. - It loads the visitor's language, places the pins of the current page and fires the
readyevent (JavaScript API). - Single-page apps work without extra code: when the URL changes, the widget reloads the pins of the new page; when the DOM changes, it re-places them.
Only one widget runs per page — a second init() is ignored. Call Pinwren.destroy() to remove it
completely (listeners, console and network wrappers, DOM).