Press c anywhere in your app to drop a pin and leave an inline comment. Emails the feedback — with a screenshot of the page and the pin drawn on it — to a configured recipient.
Framework-agnostic: works in Next.js (App Router) and Vite + Vercel serverless. Pluggable email transports: Resend, Formsubmit (zero-config), or Gmail-via-user-OAuth.
npm install github:jacksonlatka/feedback-widgetPeer deps (install if you don't have them): react, react-dom, lucide-react. NextAuth is only required if you use the /next-auth adapter.
The widget uses Tailwind utility classes, so Tailwind needs to see its source. Add to your globals.css:
@import "tailwindcss";
@source "../../node_modules/feedback-widget/src";(Adjust the relative path to wherever your CSS lives.)
// src/app/layout.tsx
import { FeedbackWidget } from 'feedback-widget';
<Providers>
{children}
<FeedbackWidget appName="My App" accentColor="#2563eb" />
</Providers>// next.config.ts
const nextConfig = {
transpilePackages: ['feedback-widget'],
};Pick a transport (see Transports below). Example with Resend:
// src/app/api/feedback/route.ts
import { createFeedbackHandler, resendTransport } from 'feedback-widget/server';
export const POST = createFeedbackHandler({
transport: resendTransport({
apiKey: process.env.RESEND_API_KEY!,
from: 'feedback@yourdomain.com',
}),
recipientEmail: 'you@example.com',
appName: 'My App',
});// src/main.tsx or wherever your app root lives
import { FeedbackWidget } from 'feedback-widget';
<YourApp />
<FeedbackWidget
appName="My App"
accentColor="#2563eb"
user={currentUser} // optional; omit to show a "Your email" field
/>// api/feedback.js (Vercel serverless, Web-standard Request/Response)
import {
createFeedbackHandler,
formsubmitTransport,
} from 'feedback-widget/server';
const handler = createFeedbackHandler({
transport: formsubmitTransport(),
recipientEmail: process.env.FEEDBACK_RECIPIENT_EMAIL,
appName: 'My App',
});
export default handler; // (request: Request) => Promise<Response>Add a rewrite so /api/feedback hits the function (usually automatic; confirm your vercel.json).
Send via Resend. 100 emails/day free. Requires a Resend account, an API key, and a verified sender address (or onboarding@resend.dev for testing).
transport: resendTransport({
apiKey: process.env.RESEND_API_KEY!,
from: 'feedback@yourdomain.com',
}),Reply-To is set to the submitter's email automatically.
Send via Formsubmit. Zero config — no account, no API key. The recipient will get a one-time confirmation email on first use; they click the link and future submissions flow through.
transport: formsubmitTransport(),Tradeoffs vs Resend: lower deliverability, the From: address is Formsubmit's domain (Reply-To is the submitter), and HTML body is rendered with Formsubmit's default template rather than your styled version. Fine for early prototypes, swap to Resend once the project matures.
Send via the signed-in user's Gmail account. Requires the consumer to have OAuth + the gmail.send scope wired up.
transport: gmailUserTokenTransport({
getAccessToken: async (request) => mySession?.accessToken,
}),The email's From: is the submitter themselves, so replies naturally go to the right person.
import { createFeedbackHandler } from 'feedback-widget/server';
import {
nextAuthGmailTransport,
nextAuthGetUser,
} from 'feedback-widget/next-auth';
import { authOptions } from '@/lib/authOptions';
export const POST = createFeedbackHandler({
transport: nextAuthGmailTransport({ authOptions }),
getUser: nextAuthGetUser({ authOptions }),
recipientEmail: 'you@example.com',
});| Prop | Type | Default | Description |
|---|---|---|---|
user |
{ name?, email? } | null |
null |
Submitter identity. If omitted, the composer shows an optional "Your email" field. |
appName |
string |
env or "App" |
Prefix in the email subject. |
endpoint |
string |
"/api/feedback" |
POST target. |
accentColor |
string |
"#111827" |
Hex color for pin, Send button, screenshot marker. |
enabled |
boolean |
true |
Force-disable. Also respects NEXT_PUBLIC_FEEDBACK_ENABLED=false. |
createFeedbackHandler({
transport, // required
recipientEmail?: string, // or FEEDBACK_RECIPIENT_EMAIL env
appName?: string, // or FEEDBACK_APP_NAME env
getUser?: (request) => Promise<{ name?, email? } | null>,
})If getUser returns a value, it overrides whatever the client sent (useful when you trust only server-side auth).
| Variable | Side | Purpose |
|---|---|---|
NEXT_PUBLIC_FEEDBACK_ENABLED |
client | "false" disables the widget without removing the mount. |
NEXT_PUBLIC_FEEDBACK_APP_NAME |
client | Fallback for the appName prop. |
FEEDBACK_RECIPIENT_EMAIL |
server | Fallback for the recipientEmail option. |
FEEDBACK_APP_NAME |
server | Fallback for the appName option. |
- Press
c(or click the floating hint) → placing mode. Cursor goes crosshair. - Click anywhere → pin drops at that spot. A composer popover anchors next to it, showing what DOM element you pointed at.
- "skip pin" button in the banner → submit general, unanchored feedback instead.
- Send → renders a full-page screenshot via
html2canvas-pro, draws the pin on top, and POSTsmultipart/form-datato the endpoint. - Server handler → builds a subject/body/HTML email and hands it to the configured transport.
Only pinned feedback captures a screenshot; general feedback skips it to save latency.
MIT