Widget configuration
Appearance now comes from Widget design
How the widget looks is set in Widget design, in your project, by describing what you want. That design is published from there and the widget fetches it on load, so the install snippet is pasted once and never edited again. If you are deciding how far you can take it, the feedback widget page walks through what the designer does, including what makes the widget white label.
The data- attributes below still work. They are applied first and a published design
overrides them, which means an existing install keeps its current look until you publish
something.
If you style the widget from your own stylesheet, read this
The widget now renders inside a closed shadow root. Rules in your stylesheet that target
#_ff-btn or #_ff-panel no longer reach it, and neither do your resets, which is the
point: the widget looks the same on every site now.
Move those rules into Widget design, where they are scoped to the widget and survive
upgrades. If you need time, add data-shadow="false" to the script tag to restore the
old rendering:
<script src="https://usefeedbackflow.com/widget.min.js"
data-api-key="YOUR_API_KEY"
data-shadow="false"
defer></script>
That attribute is a migration aid and is removed in a future release.
Setting options
There are two ways to set these, and they can be mixed.
On the script tag, as data- attributes. accentColor becomes data-accent-color, captureContext becomes data-capture-context, and so on. This keeps the install to one tag:
<script src="https://usefeedbackflow.com/widget.min.js"
data-api-key="YOUR_API_KEY"
data-accent-color="#6366f1"
data-theme="dark"
defer></script>
autoOpen has its own two attributes rather than JSON in markup: data-auto-open-paths is comma-separated and data-auto-open-delay is milliseconds.
In window.FeedbackFlowConfig, declared in a tag before the widget script. Use this for user, which is different for every visitor and so belongs in code rather than in static markup, and for anything else you would rather compute at runtime. Where both set the same option, the attribute wins, because the tag is the declaration you are looking at.
The options themselves are the same either way. Only apiKey is required; everything else falls back to the default shown.
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | None (required) | Project API key used to authenticate submissions. |
| accentColor | CSS color | #000 | Background of the launcher button and the Send button. |
| accentTextColor | CSS color | #fff | Text and foreground color shown on the accent color. |
| theme | "light" or "dark" | "light" | Panel color scheme. Use "dark" on dark-background sites so the panel, inputs, and text match. |
| position | "bottom-right" or "bottom-left" | "bottom-right" | Which corner the launcher sits in. |
| buttonText | string | "Feedback" | Launcher button label. |
| title | string | "Share feedback" | Heading shown at the top of the panel. |
| captureContext | boolean | true | Capture technical context (browser, OS, screen size, plus recent console and network errors) with each submission. Set to false to disable. |
| user | object | none | Identify the signed-in user: { email, name, id }. Attributes feedback to a customer. |
| collectEmail | boolean | false | Show an optional email field in the panel for anonymous visitors. |
| attachments | boolean | true | Let visitors attach images and PDFs. Set to false to hide the attachment control. |
| autoOpen | object | none | Open the panel automatically on matching pages: { paths, delayMs }. Omit to disable. |
| shadow | boolean | true | Render inside a closed shadow root. Set to false only to keep your own stylesheet overrides working during migration. Removed in a future release. |
Color values are sanitized before being injected into the widget CSS. An unsafe value falls back to its default.
Example: dark site with an indigo launcher
On a dark background a black launcher is invisible. Pick an accent color that contrasts and set theme: "dark" so the panel matches:
<script src="https://usefeedbackflow.com/widget.min.js"
data-api-key="YOUR_API_KEY"
data-accent-color="#6366f1"
data-accent-text-color="#fff"
data-theme="dark"
data-position="bottom-right"
defer></script>
Accent colors that contrast well on dark backgrounds: indigo #6366f1, blue #3b82f6, violet #8b5cf6, or emerald #10b981. For maximum contrast, use a white launcher with dark text (accentColor: "#ffffff", accentTextColor: "#0f172a").
Example: identify the signed-in user
If your app knows who is signed in, pass their details in user so each submission is attributed to that customer. This is the recommended way to capture identity. Render the values from your own session:
<script>
window.FeedbackFlowConfig = {
apiKey: "YOUR_API_KEY",
user: { email: "alice@acme.com", name: "Alice Rivera", id: "user_123" },
}
</script>
<script src="https://usefeedbackflow.com/widget.min.js"></script>
All three user fields are optional. email and id are used to match a customer, so repeat feedback from the same person groups together, and name is shown in your dashboard. Nothing is captured when user is omitted.
For public sites with no signed-in user, set collectEmail: true instead to show an optional email field in the panel. If both are present, user.email takes precedence over what the visitor types.
Single-page apps: update identity on login and logout
The widget reads user once at load. In an SPA where users log in without a full page reload, add the install stub before the widget script and call FeedbackFlow when auth state changes. The stub queues calls made before the widget finishes loading:
<script>
window.FeedbackFlow = window.FeedbackFlow || function () { (FeedbackFlow.q = FeedbackFlow.q || []).push(arguments) }
</script>
<script src="https://usefeedbackflow.com/widget.min.js"></script>
Then, from anywhere in your app:
// After login:
FeedbackFlow('identify', { email: 'alice@acme.com', name: 'Alice Rivera', id: 'user_123' })
// After logout:
FeedbackFlow('reset')
identify replaces the current identity (it does not merge), so pass the full set of fields each time. reset clears it, so later submissions are anonymous (or fall back to the optional email field when collectEmail is on).
Keep your usual install tag alongside it. The stub is additional, not a replacement, and the API key is still required.
Example: ask for feedback on checkout
Set autoOpen.paths to the pages where you want the panel to open on its own. Patterns are matched against the path only, so leave off the domain, the query string, and the hash:
<script src="https://usefeedbackflow.com/widget.min.js"
data-api-key="YOUR_API_KEY"
data-auto-open-paths="/checkout/**,/pricing,/app/*/settings"
data-auto-open-delay="3000"
defer></script>
Two wildcards are supported:
| Pattern | Matches | Does not match |
|---|---|---|
| /pricing | /pricing | /pricing/teams |
| /app/*/settings | /app/123/settings | /app/a/b/settings |
| /checkout/** | /checkout, /checkout/pay, /checkout/pay/card | /checkouts |
* matches within a single path segment. ** matches across segments, and a trailing /** also matches the bare prefix, so /checkout/** covers /checkout itself.
delayMs is how long to wait before opening, counted from the page being ready on a full load, or from the route change on a client-side navigation. It defaults to 3000 (three seconds) and is capped at 60000. A short delay keeps the panel from competing with your own page load (or your own route transition).
The panel auto-opens at most once per browser session. If the visitor opens or dismisses it themselves first, it will not open again on its own until their next session. Reloading the page does not retrigger it.
Single-page apps: auto-open on route changes
Auto-open works in React, Next.js, Vue, and other client-side routers with no extra setup. The widget watches for route changes and re-checks your patterns, so navigating from /app/home to /checkout/pay triggers it just like a full page load would. Matching is against the path, so hash-based routes (example.com/#/checkout) are not matched: point your patterns at routers that use the History API, not the URL hash.
Attachments
Visitors can attach screenshots and documents to a submission. This is on by default. They can click the drop zone to pick a file, drag one onto the panel, or paste an image straight from the clipboard, which makes the usual screenshot flow (Cmd+Shift+4, then Cmd+V) work with no extra steps.
Limits, enforced both in the panel and on our side:
| Rule | Value |
|---|---|
| File types | PNG, JPEG, GIF, WebP, PDF |
| Files per submission | 3 |
| Size per file | 5 MB |
Attachments appear alongside the feedback in your dashboard, with images shown as thumbnails.
To turn the feature off:
<script src="https://usefeedbackflow.com/widget.min.js"
data-api-key="YOUR_API_KEY"
data-attachments="false"
defer></script>
Content Security Policy
Files upload directly to storage rather than through our API, so a site with a strict Content-Security-Policy needs to allow that origin too. See Troubleshooting for the exact directives.