Floating feedback button
One component drops a feedback button into your app. Every report arrives with a screenshot of what the user was looking at, whatever they drew on it, the page they were on and, when they point at one, the React component behind the element.
Install
The CLI installs the SDK, mounts the widget in your app entry file and writes the key to the right env file. It detects Next.js (both routers), Vite and React Router, and never edits a file it can’t place the widget in.
npx reflet-cli initNon-interactive, for scripts and agents: npx reflet-cli init --public-key fb_pub_xxx --yes. Check an existing setup with npx reflet-cli doctor.
Or wire it up yourself
Mount it once, as the last child of your app shell. The entry ships its own "use client" directive, so a Next.js layout can stay a Server Component.
import { RefletFeedback } from "reflet-sdk/feedback";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<RefletFeedback publicKey={env.NEXT_PUBLIC_REFLET_PUBLIC_KEY} />
</body>
</html>
);
}With Vite, read the key from import.meta.env.VITE_REFLET_PUBLIC_KEY instead, and render the widget next to <App />.
Set it up with your coding agent
Paste this into Claude Code, Cursor or any agent working in the repo. It’s the same text npx reflet-cli prompt prints.
Add the Reflet feedback widget to this app.
Reflet is a feedback tool. The widget is a floating button that opens a panel
where a user writes a report; it screenshots the current viewport
automatically. On React it also lets them draw on that screenshot and point at
an element, so the report carries that element's selector and markup — plus the
component stack and source location when the build exposes them.
Read this codebase first: which package manager it uses, which framework
renders it, which file every page goes through, and how it reads the current
user. Everything below adapts to what you find — nothing here assumes Next.js.
## 1. Pick a path
- Renders React (Next.js, Vite, React Router, Remix, TanStack Start, Expo web,
Astro islands…) → §2, the npm package. Full annotation and element picker.
- Anything else (Vue, Svelte, Angular, Rails, Django, Laravel, plain HTML) →
§3, the script tag. Report, screenshot and board; no annotation or picker.
## 2. React
Install `reflet-sdk` with the package manager this repo already uses.
Mount the widget exactly once, as the last child of the app shell — the single
component every page renders through. Find that file; it is usually one of:
- Next.js App Router → `app/layout.tsx`, just before `</body>`. The widget
ships its own `"use client"`, so the layout stays a Server Component.
- Next.js Pages Router → `pages/_app.tsx`, sibling of `<Component />`.
- Vite → beside `<App />` in `src/main.tsx`.
- React Router / Remix → before `</body>` in `app/root.tsx`.
- TanStack Start → the root route's shell component.
```tsx
import { RefletFeedback } from "reflet-sdk/feedback";
<RefletFeedback publicKey={/* see §4 */} />
```
## 3. Any other stack
Serve one script tag on every page, from the layout or base template this app
already has:
```html
<script
src="https://www.reflet.app/widget/reflet-feedback.v1.js"
data-public-key="fb_pub_xxx"
data-position="bottom-right"
data-theme="auto"
defer
></script>
```
Anything beyond key, position, colour and theme goes through a config object
declared before the script loads:
```html
<script>
window.Reflet = {
publicKey: "fb_pub_xxx",
user: { id: "user_123", email: "jane@example.com", name: "Jane Doe" },
};
</script>
```
## 4. The public key
`fb_pub_xxx`
It is safe in the browser and comes from Reflet Dashboard → In-App. Put it in
whichever env file this framework reads, under whichever prefix that framework
exposes to the browser — `NEXT_PUBLIC_`, `VITE_`, `NUXT_PUBLIC_`,
`PUBLIC_`, `EXPO_PUBLIC_`, `REACT_APP_`. Read it the way the rest of this
codebase reads its own public env vars; do not invent a second convention.
## 5. Identity
Reports carry an identity instead of asking for an email. Find how this
codebase reads its session and reuse it — do not invent an auth hook. If there
is no auth, skip this section.
Unsigned, when the app is not security-sensitive about who files a report:
```tsx
<RefletFeedback publicKey={KEY} user={{ id: user.id, email: user.email, name: user.name }} />
```
Signed, so a browser cannot impersonate another user — sign on the server and
hand the token down. `signUser` is runtime-agnostic (Node, Deno, Bun, Edge,
Cloudflare Workers, Convex), so it fits a route handler, a server function, a
middleware or a controller alike:
```ts
import { signUser } from "reflet-sdk/server";
const { token } = await signUser(
{ id: user.id, email: user.email, name: user.name },
REFLET_SECRET_KEY
);
```
```tsx
<RefletFeedback publicKey={KEY} userToken={token} />
```
The secret key never reaches the browser: read it from a server-only env var,
never one carrying a public prefix. The script tag takes the same `userToken`
through `window.Reflet`.
## 6. Options worth setting when they match this app
- `position` — "bottom-right" (default), "bottom-left", "top-right",
"top-left". Check what already sits in that corner before choosing.
- `primaryColor` — any CSS colour, to match the product's brand.
- `theme` — "auto" (default), "light", "dark". If this app owns a theme
toggle, pass its current value rather than leaving it on "auto".
- `enabled` — gate the launcher on an app-owned boolean, for example
`enabled={user.email === "you@example.com"}` while dogfooding. Gate with
this prop, never by leaving the component unrendered: the dev tools below and
team tools keep working while the launcher is hidden.
- `dismissForDays` — let a reporter hide the launcher for that many days.
- `hotkey` — e.g. "mod+shift+f"; off by default so nothing is hijacked.
- `captureConsole` — set to false to stop recording console errors.
- `metadata` — a flat string record merged into every report (plan, tenant…).
## 7. Dev tools for the team (React only)
In development, the same widget adds a small draggable bar above the
launcher: pick an element to write a note on it (pinned on the page, kept in the browser's
IndexedDB) or Shift+click it to open its source, copy notes as a prompt for a
coding agent, send them to the board as internal items that stay off the
public board, and read the board's feedback for the current page. Production
builds never load it; pass `devtools={false}` to turn it off.
Code view, code search, the board, Connect and sessions go through one
dev-only route:
- Next.js App Router → `app/api/reflet-devtools/[...path]/route.ts`:
`import { createDevtoolsRoute } from "reflet-sdk/devtools/next";` then
`export const { GET, POST } = createDevtoolsRoute();` — the handlers answer
404 unless `NODE_ENV` is `development`. A route handler cannot see who
calls it, so bind the dev script to loopback: `next dev -H 127.0.0.1`. If
the dev script binds another host on purpose (Docker, a devcontainer, phone
testing), leave it and tell the developer the route is reachable from their
network.
- Vite → add `refletDevtools()` from `reflet-sdk/devtools/vite` to
`plugins`; it only runs in `vite dev` and refuses other machines.
- Anything else → skip it; notes and prompts still work without it.
To reach the board, the developer clicks Connect to Reflet in the devtools panel footer;
the dev server keeps a revocable token outside the repository. Setting
`REFLET_SECRET_KEY` (server-only, never a public prefix) replaces Connect.
`REFLET_EDITOR` (vscode, cursor, windsurf, zed, webstorm) picks the editor
for "Open in editor" links.
## Constraints
- Mount it exactly once. Two widgets means two floating buttons.
- Do not wrap it in `RefletProvider` unless the app already uses one; the
widget works standalone with a `publicKey`.
- It needs the DOM: keep it out of server-only files other than the app shell.
- Do not commit a real key if this repo commits its env files.
- Match this codebase's conventions — its formatter, its import style, its file
layout. The integration should read like the code around it.
When you are done, run the app, click the button and confirm the panel opens
with a screenshot preview.What ends up on a report
- Screenshot. Rendered from the DOM, so there is no screen-share permission prompt. The widget leaves itself out of its own capture.
- Drawing. Pen, arrow, box, highlight and a redaction tool that pixelates a region before anything leaves the browser. Both the clean and the annotated image are stored.
- Element. Point at anything on the page and the report highlights it in the screenshot, with the page region it sits in, its redacted markup, a selector that resolves back to it, the React component stack, and the source file and line when the build exposes them.
- Page context. URL, title, browser, OS, device, viewport, locale and timezone.
- Console. The last 30 errors and warnings the page logged, including uncaught errors and rejected promises.
Props
Identified users
Pass the current user and the widget stops asking for an email. Add your own metadata to slice reports by plan, tenant or release.
<RefletFeedback
publicKey={env.NEXT_PUBLIC_REFLET_PUBLIC_KEY}
user={{ id: user.id, email: user.email, name: user.name }}
metadata={{ plan: user.plan, tenant: user.orgSlug }}
hotkey="mod+shift+f"
/>Devtools for your team
On your dev server, the same component adds a small bar above the launcher that you can drag anywhere. Pick an element to write a note on it, press ⌘C to copy it with its source, or Shift-click it to open its source with the JSX highlighted. Copy your notes as one prompt for a coding agent, or add them to the board as internal feedback that only members see. The Board tab lists the feedback reported on the current page, with the element and its code one click away. Production builds never ship it.
Code view, the board, Connect and sessions go through one dev-only route. To reach the board, click Connect to Reflet in the panel footer and approve your dev server on reflet.app: it keeps a revocable token outside your repository. You can also set REFLET_SECRET_KEY on the server instead. Set REFLET_EDITOR to cursor, zed, windsurf or webstorm for the editor links.
// Next.js: app/api/reflet-devtools/[...path]/route.ts
import { createDevtoolsRoute } from "reflet-sdk/devtools/next";
export const { GET, POST } = createDevtoolsRoute();
// package.json: a route handler cannot see who calls it, so stay on loopback.
// Binding another host on purpose (Docker, phone testing) exposes the route to that network.
"dev": "next dev -H 127.0.0.1"
// Vite: vite.config.ts
import { refletDevtools } from "reflet-sdk/devtools/vite";
export default defineConfig({ plugins: [react(), refletDevtools()] });Sessions
Talk through your app while you use it. A session records your voice, what you point at and mark, and a screenshot at the start, at each mark and on each page change. When you finish, Reflet transcribes it and proposes the changes you asked for, each with the quotes and screenshots behind it. Keep the ones you want, then copy them as a brief for a coding agent, export a .zip with the screenshots, or create internal feedback.
On your dev server, sessions live in the devtools Sessions tab. On a deployed app, open any page with ?reflet-team, sign in with Reflet and approve the app: members of your organization get the same tools, your users never see them.
- The widget’s public key must list the app’s exact origin under Allowed domains. A key without allowed domains signs no one in.
- The page must allow the microphone. A
Permissions-Policyheader withmicrophone=()blocks it; usemicrophone=(self). - Pause stops all collection. Keystrokes are never recorded, form fields appear empty in screenshots, and password fields and
data-reflet-redactelements are left out. - Voice is transcribed with zero data retention, and the audio is deleted once transcribed, within 24 hours at most.
Good to know
- The panel lives in a shadow root. Your CSS can’t reach it and its CSS can’t reach your app.
- Component names and source locations come from React’s debug data. Development and preview builds give you
src/billing/invoice-row.tsx:42:7. Production builds strip that, so reports fall back to the component stack and the selector. - Screenshots are rendered from the DOM. Cross-origin images without CORS headers, iframes and canvas content may come out blank.
- A failed screenshot upload never loses the written report. The feedback is created first and the image is attached after.
- The markup of a picked element is scrubbed before it leaves the browser: typed-in values, emails and token-shaped strings are replaced. Mark a subtree with
data-reflet-redactto keep its contents out of reports entirely. - Report context is only visible to members of your organization, not to visitors on a public board.
- Your organization doesn’t have to be public. The public key writes reports and nothing else; reading the board still needs a member session or a secret key.
- A public key can send 30 reports per minute, shared by every visitor. Screenshot uploads don’t count toward that, and a report takes at most 10 screenshots. Past the limit the API answers
429and the panel shows the error.