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 cannot place the widget in.

npx reflet-cli init

Non-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={process.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 is the same text npx reflet-cli prompt prints.

Setup prompt
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.
- `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…).

## 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 excludes itself from 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 carries a close-up of it, 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

PropTypeDefaultDescription
publicKeystringYour fb_pub_… key. Optional when the app is wrapped in RefletProvider.
user{ id, email?, name?, avatar? }Who is reporting. Skips the email field and links the report to that user.
position"bottom-right" | "bottom-left" | "top-right" | "top-left""bottom-right"Which corner the button sits in.
enabledbooleantrueRender the widget. Pass a boolean to expose it to staff or beta users only.
dismissForDaysnumberAdds a panel action that hides the launcher in this browser for the chosen number of days.
captureOnOpenbooleantrueScreenshot the viewport as soon as the panel opens. Set to false to make it opt-in.
captureConsolebooleantrueRecord console errors and warnings and attach the last 30 to the report.
hotkeystring | nullnullShortcut that toggles the panel, e.g. "mod+shift+f". Off by default so no app shortcut is hijacked.
theme"auto" | "light" | "dark""auto"Follows the OS colour scheme unless forced.
primaryColorstringAny CSS color. Drives the button and the accents.
offsetnumber20Distance in pixels between the button and the viewport edge.
metadataRecord<string, string>Flat string record merged into every report — plan, tenant, release…
categories("bug" | "idea" | "question")[]["bug", "idea", "question"]Which category chips to show. One category hides the picker.
labelsPartial<FeedbackWidgetLabels>Override any string in the panel for i18n.
onSubmit(result: { feedbackId: string }) => voidFires with the created feedback id after a successful send.

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={process.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"
/>

Good to know

  • The panel lives in a shadow root. Your CSS cannot reach it and its CSS cannot 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, 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-redact to 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 does not 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 is capped at 30 reports per minute. Past that the API answers 429 and the panel shows the error.