Feedback widget (@usero/sdk)
A drop-in feedback button for the web. Vanilla JS, React component, or a <script> tag. The widget renders a floating button;
users click it to leave a 1 to 4 rating, an optional comment, and optional screenshots. Everything lands in your dashboard
automatically.
The package is open source (github.com/usero-feedback/usero) and small: the vanilla build never imports React, so Vue, Svelte, Angular, plain HTML, and Electron apps pay zero React tax.
Install
npm install @usero/sdkReact
Mount it once in your root layout (it renders a fixed-position floating button, so do not put it inside individual pages):
import { UseroFeedbackWidget } from '@usero/sdk/react'
export function App() {
return (
<>
{/* your app */}
<UseroFeedbackWidget clientId='client_3611132dc3e64aa3' environment={process.env.NODE_ENV} />
</>
)
}Vanilla JS (Vue, Svelte, Angular, Astro, Electron, anything)
Call it once at app startup:
import { initUseroFeedbackWidget } from '@usero/sdk'
const widget = initUseroFeedbackWidget({
clientId: 'client_3611132dc3e64aa3',
environment: import.meta.env.MODE,
})
// later, if you need to remove it:
widget.destroy()Script tag (CDN, no bundler)
Add these two tags just before </body>:
<script src="https://unpkg.com/@usero/sdk"></script>
<script>
Usero.initUseroFeedbackWidget({ clientId: 'client_3611132dc3e64aa3' })
</script>unpkg and jsDelivr both serve the IIFE bundle automatically.
Warning
Omit the environment option for your default environment. Do not pass a placeholder like "no-env" or
"default". The dashboard treats an absent environment as the default; a placeholder string creates a separate environment and
your feedback will not appear in the default inbox.
Identify your users (optional)
The widget works fully anonymous. If you have a logged-in user, identifying them lights up "who is this person" on session replays and lets you filter feedback by email in the dashboard.
React: pass the user prop (omit or pass null for logged-out visitors). The widget re-identifies automatically when the prop
changes:
<UseroFeedbackWidget
clientId='client_3611132dc3e64aa3'
user={{ id: currentUser.id, email: currentUser.email, displayName: currentUser.name }}
/>Vanilla: pass a getUser callback, called at session start:
initUseroFeedbackWidget({
clientId: 'client_3611132dc3e64aa3',
getUser: () => (currentUser ? { id: currentUser.id, email: currentUser.email } : null),
})What's new (changelog in the widget)
Set whatsNew: true and the widget panel shows two tabs, Feedback and What's new. The second tab lists your published
changelog entries, newest first, and the floating launcher shows an unread dot when there are entries the user has not opened yet.
Each entry links out to its page on your public board if you have one enabled.
<UseroFeedbackWidget clientId='client_3611132dc3e64aa3' whatsNew user={{ id: currentUser.id, email: currentUser.email }} />Only published changelog entries appear. The feed serves entries you have published from the changelog admin on your project's board; drafts are never returned, and unpublishing an entry removes it from the feed. With zero published entries the tab has nothing to show, so publish at least one first. See the public board docs for authoring entries, including drafting them from your merged pull requests.
"Shipped for you"
For identified users, the tab can open with a pinned Shipped for you section: their original feedback quoted back, plus the title of the merged pull request that resolved it. The data comes from the chain Usero already keeps: a published changelog entry links to merged pull requests, each PR links to the feedback that prompted it, and that feedback carries the submitter's email. When the email on the feedback matches the email of the current user (case-insensitive), the section appears.
Two things have to be true for a user to see it:
- Their original report had an email on it. Either they ticked the share-my-email option when submitting, or your app had identified them at the time.
- They are identified now. The widget sends the current user's email when it loads the feed (
userprop in React,getUserin vanilla). Anonymous visitors see the changelog feed and the unread dot, but never the personal section.
There is no dedupe stamp on the widget side: a shipped request keeps appearing in Shipped for you whenever the user opens the tab. The one-time requester emails sent on publish are a separate mechanism with their own dedupe; turning those off does not affect the widget tab.
Under the hood the tab reads GET /api/changelog?clientId=...&email=..., a public JSON endpoint you can also call directly if you
are building your own UI.
Traits and segments
The user object also takes a traits bag of primitives, anything your app knows about the person:
<UseroFeedbackWidget
clientId='client_3611132dc3e64aa3'
user={{
id: currentUser.id,
email: currentUser.email,
traits: { plan: currentUser.plan, seats: currentUser.seats, mrr: currentUser.mrr },
}}
/>Usero stores the latest-seen traits on the person. A numeric mrr trait is promoted to a queryable field that powers the revenue
rollups on the dashboard and the revenue signal in prioritize value scores.
Traits are what segments run on. A segment is a saved filter over your identified people (conditions on MRR, email domain, or any trait, all must match), built on the dashboard's Segments page. Apply one as a lens on the dashboard or prioritize view and the feedback counts, MRR rollup, and value scores recompute to just those people. Anonymous feedback is hidden while a lens is applied, since an unidentified person cannot be matched against segment rules. Membership is evaluated live, so a changed trait moves the person to the right segments on the next page load.
Options
| Option | Type | Default | Description |
|---|---|---|---|
clientId |
string |
required | Your Usero client id. See Find your clientId. |
position |
'left' | 'right' |
'right' |
Which side of the viewport the button sits on. |
theme |
Partial<WidgetTheme> |
auto | Override colors. By default the widget follows the OS color scheme (prefers-color-scheme), with dark as the fallback. Explicit values win; partial overrides merge on top of the detected base. |
title |
string |
'Share Feedback' |
Panel header. |
placeholder |
string |
'Tell us what you think... (optional)' |
Comment placeholder. |
showEmailOption |
boolean |
true |
Show the "share my email" checkbox. |
showScreenshotOption |
boolean |
true |
Show the screenshot upload button (up to 3 images, 10MB each). |
whatsNew |
boolean |
false |
Add a "What's new" tab with your published changelog entries, an unread dot on the launcher, and a personal "Shipped for you" section for identified users. See above. |
environment |
string |
undefined | Tag feedback with an environment. Omit for your default environment, see the warning above. |
metadata |
Record<string, unknown> |
undefined | Arbitrary metadata attached to every submission. |
baseUrl |
string |
'https://usero.io' |
Override the API host (self-hosted Usero). |
plugins |
UseroPlugin[] |
undefined | Opt-in plugins, for example session replay. |
getUser |
() => User | null |
undefined | Vanilla only. Returns the current logged-in user (or null for anonymous). React uses the user prop instead. |
onSubmit |
(data) => void |
undefined | Fires after a successful submission. |
onError |
(err: Error) => void |
undefined | Fires on init or submission error. |
onOpen / onClose |
() => void |
undefined | Fire when the panel opens or closes. |
Content Security Policy
If your app has a strict CSP, allow https://usero.io in connect-src (and https://unpkg.com in script-src if you use the
script tag). The widget makes no other cross-origin requests.
Next
- Session replay: record sessions and link each feedback to the moment it was submitted
- Headless: building your own UI? Keep the widget's submission pipeline, identity, and plugins, drop ours
- POST /api/feedback: the raw API the widget submits to