Install the Feedback Embed: Script Tag, npm and Browser Support
The script tag, the JSON config block, npm, the UMD build, self-hosting, which browsers it runs in, and every option the widget takes.
Paste one script tag and the widget loads a 17 KB core, then fetches targeting, the message box, screenshots and video only when they are used. It needs Chrome 123, Firefox 120 or Safari 17.5 to draw the message box properly. Every option, its default and its script attribute are in the table at the end.
This page covers every way to load the thumbs widget, what each one downloads, and every option it takes. For the first install from the dashboard, start with the Dashboard quick start.
Paste the script tag
<script
src="https://unpkg.com/@qaiddev/thumbs-embed@1.9.0/dist/embed.js"
defer
data-api-key="YOUR_PROJECT_KEY"
data-endpoint="https://qaid.dev/api/feedback"
></script>
Put it just before </body>. defer makes the browser run it after the page is parsed, so it never holds the page up and it finds a theme <style> block wherever that block sits. embed.js is 0.5 KB gzipped. It copies every data-* attribute onto a new tag that loads loader.js from the same folder, and the widget starts once the page has been parsed. The dashboard’s snippet leaves out the @1.8.0, which gets you the newest release on every load. Keep the version in to pin it: the other files load from the same versioned folder.
Keep the config out of the tag
Every option has a data-* attribute on the tag (thumbs-embed 1.9.0 or later). If a long list of attributes is hard to read, put the same settings in a JSON block instead:
<script type="application/json" data-feedback-config>
{
"endpoint": "https://qaid.dev/api/feedback",
"apiKey": "YOUR_PROJECT_KEY",
"position": "bottom-left",
"text": { "positiveLabel": "Envoyer un avis positif" }
}
</script>
<script src="https://unpkg.com/@qaiddev/thumbs-embed@1.9.0/dist/embed.js" defer></script>
When the block is there and is valid JSON, the widget uses it and ignores the tag’s data-* attributes, so it must carry endpoint and apiKey itself. If the JSON does not parse, the widget falls back to the attributes without a word. "cssSelector": "#qaid-theme" works here like data-css-selector.
Install from npm
npm install @qaiddev/thumbs-embed
import { QaidFeedback } from "@qaiddev/thumbs-embed";
const feedback = new QaidFeedback({
endpoint: "https://qaid.dev/api/feedback",
apiKey: "YOUR_PROJECT_KEY",
});
// feedback.destroy() removes it again.
Call new in the browser, after the page has a <body>: in a framework, that means in a mounted hook, not during server rendering. The main entry is one 32.7 KB file with every feature in it. Import from @qaiddev/thumbs-embed/loader instead to get the same class with the split, load-on-use build. Drop the script tag once you construct the widget in code, or the page gets two widgets.
Use it in a page with no build step
A module script can import the loader straight from the CDN:
<script type="module">
import { QaidFeedback } from "https://unpkg.com/@qaiddev/thumbs-embed@1.9.0/dist/loader.js";
new QaidFeedback({
endpoint: "https://qaid.dev/api/feedback",
apiKey: "YOUR_PROJECT_KEY",
});
</script>
This is the form to use when the widget should only start on some pages. See Conditional display.
Use the UMD build
dist/qaid.umd.cjs is the whole widget in one classic script, 26.7 KB gzipped. It starts itself from the data-* attributes on its own tag, and it defines window.Qaid, so new Qaid.QaidFeedback({ … }) works after it loads.
It does not rescue old browsers. embed.js switches to it only in a browser without ES modules, but the file is written in modern JavaScript that such a browser cannot read. Use it when you want one file to host, not for reach. If your server sends .cjs files with a type other than JavaScript, rename it to .js.
Host it yourself
Copy node_modules/@qaiddev/thumbs-embed/dist/ to your server, keeping embed.js, loader.js and the chunks/ folder side by side, and point the tag’s src at your copy of embed.js.
Two parts still load from elsewhere:
- Screenshots on touch screens, or with
screenshotMethod: "dom", loadhttps://qaid.dev/lib/html2canvas.min.js. - A button linked to a quest imports the quests widget from unpkg. Set
quests.moduleUrl(data-quest-module-url) to your own copy.
Know what loads, and when
| Part | Size, gzipped | Loaded |
|---|---|---|
embed.js |
0.5 KB | With the page |
| Core: buttons, styles, submit | 16.9 KB | With the page |
| Element targeting | 3.8 KB | When the pointer rests on a thumb, or on the first click |
| Message box | 2.5 KB | On the first thumb click |
| Screenshot, then the markup editor | 0.7 to 1.1 KB, then 5.1 KB | Soon after the page settles if captureScreenshot is on, otherwise on the first capture |
| Video recording | 6.7 KB | Soon after the page settles if captureVideo is on, and when the pointer rests on the record button |
To watch it, filter the Network panel to JS and reload. You should see embed.js, loader.js and a loader- file, then a targeting- file when you hover a thumb. If nothing loads, check the tag and look for a blocked request; see Troubleshooting.
Check which browsers it runs in
thumbs-embed 1.8.0 or later runs on plain http:// pages as well as https://. Screen recording still needs https:// or localhost.
| The widget uses | For | Chrome and Edge | Firefox | Safari |
|---|---|---|---|---|
light-dark() |
Background and text of the message box, phone sheet and video review | 123 | 120 | 17.5 |
color-mix() |
Targeting outline, crosshairs and focus glow | 111 | 113 | 16.2 |
:has() |
Sizing emoji icons | 105 | 121 | 15.4 |
| Screen capture | The record button, and desktop screenshots | Desktop only | Desktop only | Desktop only |
In a browser older than the first row, the buttons still work, but the message box has no background. Where screen capture is missing, the record button does not appear.
A strict Content Security Policy needs unpkg.com (and qaid.dev, for phone screenshots) in script-src, qaid.dev in connect-src, and 'unsafe-inline' in style-src, because the widget writes its own <style> elements.
Every option
JavaScript and the JSON block take the option names. The script tag takes the attributes. For a true/false attribute, write "true"; anything else, or a bare attribute, leaves it off.
| Option | Type | Default | Script attribute |
|---|---|---|---|
endpoint |
URL, required | none | data-endpoint |
apiKey |
string | none | data-api-key |
position |
"bottom-right", "bottom-left", "top-right", "top-left" |
"bottom-right" |
data-position |
offset |
{ x, y } in pixels |
{ x: 16, y: 16 } |
data-offset-x, data-offset-y |
direction |
"horizontal", "vertical" |
"horizontal" |
data-direction |
container |
CSS selector | none: fixed to the window | data-container |
zIndex |
number | 50 |
data-zindex |
buttonSize |
"small", "medium", "large" |
"medium" |
data-button-size |
buttonClass |
class names | none | data-button-class |
colors.positive |
CSS colour | rgb(0, 200, 83) |
data-positive-color |
colors.negative |
CSS colour | rgb(255, 0, 0) |
data-negative-color |
colors.marker |
CSS colour | #6365f1 |
data-marker-color |
positiveIcon, negativeIcon, feedbackIcon |
SVG or text | built-in icons | data-positive-icon, data-negative-icon, data-feedback-icon |
recordIcon |
SVG or text | built-in icon | none |
singleButton |
true/false | false |
data-single-button |
hideThumbs |
true/false | false |
data-hide-thumbs |
hideDismiss |
true/false | false, and always on in a container |
data-hide-dismiss |
incognito |
true/false | false |
data-incognito |
skipTargeting |
true/false | false |
data-skip-targeting |
feedbackMode |
"target", "annotate" |
"target" |
data-feedback-mode |
text.* |
strings | see Text customization | same page |
text.errorTitle, text.errorMessage |
strings | “Message not sent”, “Something went wrong, so we did not get your message. Please try again later.” | data-error-title, data-error-message |
hideConfirmation |
true/false | false |
data-hide-confirmation |
modalWidth |
pixels | 400 |
data-modal-width |
backdropOpacity |
0 to 1 | 0.3 |
data-backdrop-opacity |
fontFamily |
CSS font list | system-ui, -apple-system, sans-serif |
data-font-family |
fontSize |
pixels | 16 |
data-font-size |
css |
CSS text | none | data-css-selector, naming a <style> element |
captureScreenshot |
true/false | false |
data-capture-screenshot |
screenshotMethod |
"permission", "dom" |
"permission", and "dom" on touch screens |
data-screenshot-method |
screenshotOptions.quality |
0 to 1 | 0.8 |
data-screenshot-quality |
screenshotOptions.maxWidth, .maxHeight |
pixels | 1280, 800 |
data-screenshot-max-width, data-screenshot-max-height |
annotate |
true/false | true |
data-annotate="false" turns it off |
annotationColor |
CSS colour | colors.marker |
data-annotation-color |
annotationPalette |
list of hex colours | six colours | none |
captureVideo |
true/false | false |
data-capture-video |
videoOptions.maxDuration |
seconds | 15 |
data-video-max-duration |
videoOptions.redaction |
true/false | false |
data-video-redaction |
quests.base |
URL | none | data-quest-base |
quests.up, .down, .video |
quest ids | none | data-quest-up, data-quest-down, data-quest-video |
quests.apiKey |
string | apiKey |
data-quest-api-key |
quests.moduleUrl |
URL | the quests widget on unpkg | data-quest-module-url |
Screenshots, console errors and video recordings are kept on Pro projects only. On Free the feedback still arrives without them. See Free vs Pro.