Skip to content

jacksonlatka/feedback-widget

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

feedback-widget

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.

Install

npm install github:jacksonlatka/feedback-widget

Peer deps (install if you don't have them): react, react-dom, lucide-react. NextAuth is only required if you use the /next-auth adapter.

Wire up Tailwind v4

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.)

Next.js setup

1. Mount the widget

// src/app/layout.tsx
import { FeedbackWidget } from 'feedback-widget';

<Providers>
  {children}
  <FeedbackWidget appName="My App" accentColor="#2563eb" />
</Providers>

2. Tell Next.js to transpile the package

// next.config.ts
const nextConfig = {
  transpilePackages: ['feedback-widget'],
};

3. Create the API route

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',
});

Vite + Vercel serverless setup

1. Mount the widget

// 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
/>

2. Vite doesn't need transpilePackages — it compiles node_modules fine.

3. Create the serverless function

// 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).

Transports

resendTransport({ apiKey, from })

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.

formsubmitTransport()

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.

gmailUserTokenTransport({ getAccessToken })

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.

NextAuth convenience (feedback-widget/next-auth)

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',
});

Props

Prop Type Default Description
user { name?, email? } | null null Submitter identity. If omitted, the composer prompts for name (required) and email (optional) the first time, then remembers them.
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.
persistUserInfo boolean true Remember submitter name/email in localStorage so they aren't re-prompted on future sessions. Set to false to manage state yourself. Ignored when user is provided.

Remembering the submitter

When no user prop is passed, the composer asks for a name (required) and email (optional) on first submit. Both are stored in localStorage under feedback-widget.userName and feedback-widget.userEmail, and the inputs are hidden on subsequent opens — the composer shows a "Submitting as {name} · change" link instead. Clicking change clears the stored name and re-shows the input. Pass persistUserInfo={false} to opt out, or clear the keys manually to reset.

Server handler options

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).

Env vars (all optional)

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.

How it works

  • 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 POSTs multipart/form-data to 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.

Theming

The composer uses CSS custom properties with light-mode fallbacks, so it adapts to your app's theme automatically. Define any of these on :root (or a [data-theme="dark"] block) to override:

  • --surface-background, --surface-2
  • --border-secondary, --border-tertiary
  • --text-on-surface, --text-secondary, --text-hint
  • --red-light / --red-dark, --green-light / --green-dark

If none are defined, the widget renders in its original light theme.

Note: because Tailwind v4 generates these var(...) arbitrary-value classes on demand, your consuming app's Tailwind setup must include the widget's source via @source (see Wire up Tailwind v4).

License

MIT

About

A simple tool for capturing user feedback in live prototypes

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors