The widget
One script tag, one floating button, a feedback modal that drops onto any page. Everything below is a knob you might want to turn — but you don't have to.
The script tag
Paste this into the HTML of any page you want to collect feedback on. A floating button appears in the corner; click it and a feedback modal opens.
<script
src="https://opini.dev/widget.js"
data-opini-key="pk_live_..."
data-opini-label="Send feedback"
data-opini-position="bottom-right"
data-opini-theme="light"
data-opini-accent="#C2410C"
defer></script>Running Opini on a different origin? Replace opini.dev with your own host. The widget figures out where to send submissions from there.
Options
All the knobs live on the script tag as data-* attributes. Only the key is required.
| Attribute | Required | What it does |
|---|---|---|
| data-opini-key | yes | Your project's public widget key (pk_live_...). |
| data-opini-label | no | Text on the floating button. Defaults to "Send feedback". |
| data-opini-position | no | Which corner to park the button in. bottom-right (default), bottom-left, top-right, top-left. |
| data-opini-theme | no | light (default) or dark. Pick whichever matches your page. |
| data-opini-accent | no | Your brand color for the submit button and focus rings. Any CSS color — hex, rgb(), or a name. |
| data-opini-base | no | Point at a different API host. Rarely needed — the script works this out from its own src. |
Server-side customization
The data-* attributes above are the fallback. On mount the widget fetches its project config from your Opini instance and lets an admin override a handful of strings without touching the script tag on your page:
widget_label— the floating button text.widget_headline— the modal's heading.widget_placeholder— the textarea placeholder copy.widget_submit_label— the submit button.widget_thanks— the thank-you state after a successful send.accent_hex,widget_theme,widget_position— visuals.
Precedence: server wins, data-* fills the gaps, hard-coded defaults catch the rest. So operators can retheme every widget across every customer site from a single place without asking anyone to re-paste a script tag.
If the config fetch fails — offline, blocked, CORS — the widget still mounts with attribute + default values. The network call is never on the critical path.
Identifying users
Tell Opini who's sending feedback — so it lands in Triage with a name and anything else you care to pass along.
window.opini.identify({
email: "[email protected]",
name: "Ada Lovelace",
plan: "pro",
signed_up_at: "2024-11-02"
});A few notes:
email— shows up in Triage only. Never rendered on public pages.name— used as the display name in Triage and on admin views.- Everything else (plan, signup date, anything) rides along as extra context on the Triage detail panel.
Call it whenever you know who the user is — on page load, after login, doesn't matter. The last call before a submission wins.
Theming
Two knobs. data-opini-theme picks light (default, white surface) or dark (inverted). The widget lives inside a shadow root, so your page's CSS can't bleed in and break it.
data-opini-accent is your brand color — it paints the submit button and the focus rings. Any valid CSS color works.
Follow-ups
Triage isn't a one-way inbox. Every piece of feedback carries a follow-up button in the admin view — ask the submitter a clarifying question and they'll get a one-click reply link in their email. No account, no login, just one focused page with the original feedback excerpt, the question, and a textarea.
The reply lands back on the same feedback row. Follow-up links expire after 30 days and can only be answered once. Full page at /docs/follow-ups.
Accessibility
Everything you'd hope for is already taken care of:
- The modal announces itself to screen readers as a dialog with a labelled heading.
- Keyboard users can tab through the form without leaking focus back to the page; Escape closes the modal and returns focus to the button that opened it.
- The floating CTA is a real button — no div-pretending-to- be-a-button tricks, no tap targets smaller than a fingertip.
- The accent color flows into focus rings, so keyboard-first users always know where they are.
Removing it
Delete the script tag. The widget doesn't touch localStorage, sessionStorage, or cookies on your page, so there's nothing left behind.
Advanced
You probably don't need any of this. Open if you do.
Running a tight Content Security Policy?
Here's what to allow (assuming Opini is hosted at opini.dev):
Content-Security-Policy:
script-src 'self' https://opini.dev;
connect-src 'self' https://opini.dev;
style-src 'self' 'unsafe-inline';The widget injects its styles inline into its shadow root, so 'unsafe-inline' is needed on style-src until we ship nonce-based style loading. No eval, no external images, no frames.
Exact bundle size
The widget is a single script — no runtime deps, no lazy chunks, nothing to wire up beyond the tag.
- Gzipped: ~4.2 KB
- Uncompressed: ~12 KB