Skip to content

Feedback Button in React

  • Nordva Launch account with a publishable API key (nv_pub_...)
  • React 17+ or Next.js

Drop this before </body> in your HTML:

<script src="https://cdn.nordva.dev/v1/feedback.js"
data-key="nv_pub_live_YOUR_KEY"
data-position="bottom-right"
data-accent="#F59E0B"
data-user-id="OPTIONAL_USER_ID"
data-user-email="OPTIONAL_USER_EMAIL">
</script>

A floating “Feedback” button appears in the corner. Clicking it opens a drop-up modal. Submitted feedback is auto-classified by Claude Haiku and routed to your configured destinations (email, Slack, Linear).

Attributes:

AttributeDefaultDescription
data-keyrequiredYour publishable key (nv_pub_...)
data-positionbottom-rightbottom-right or bottom-left
data-accent#F59E0BButton and submit color (hex)
data-user-idYour app’s user identifier — stored for filtering
data-user-emailThe signed-in user’s email, so you can reply to their feedback
data-ask-emailfalseSet to true to add an optional “Email (if you want a reply)” field. Ignored when data-user-email is set

The email is optional in every form. When one is given it is returned as email on GET /v1/feedback, shown under Reply to in the dashboard and included in the MCP list_feedback results. It is not forwarded to your routing destinations, so it never lands in a public GitHub issue.

Every submission has a status. Launch sets received, classified, routed, partial_routed or failed_classification; you set reviewed or resolved:

Terminal window
curl -X PATCH https://api.nordva.dev/v1/feedback/fb_01J... \
-H "Authorization: Bearer nv_live_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "resolved" }'

Send classification in the same call to correct the classifier. A correction gets confidence 1 and is not routed again, so it never creates a second issue. GET /v1/feedback?status=open lists everything not yet reviewed or resolved. From an MCP client, ask your assistant to mark an item resolved; it uses update_feedback.

For full UI control:

'use client';
import { useState } from 'react';
interface FeedbackButtonProps {
publishableKey: string;
userId?: string;
/** Optional. Lets you reply to the person who sent the feedback. */
userEmail?: string;
}
export function FeedbackButton({ publishableKey, userId, userEmail }: FeedbackButtonProps) {
const [open, setOpen] = useState(false);
const [message, setMessage] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'done' | 'error'>('idle');
async function submit() {
if (message.trim().length < 5) return;
setStatus('loading');
const res = await fetch('https://api.nordva.dev/v1/feedback', {
method: 'POST',
headers: {
'Authorization': `Bearer ${publishableKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
feedback_text: message.trim(),
user_id: userId,
email: userEmail,
page_url: window.location.href,
}),
});
if (res.ok) {
setStatus('done');
setTimeout(() => { setOpen(false); setStatus('idle'); setMessage(''); }, 2000);
} else {
setStatus('error');
}
}
return (
<>
<button
onClick={() => setOpen(o => !o)}
style={{ position: 'fixed', bottom: 20, right: 20 }}
>
Feedback
</button>
{open && (
<div style={{ position: 'fixed', bottom: 72, right: 20, width: 280, background: '#fff', borderRadius: 12, boxShadow: '0 8px 32px rgba(0,0,0,0.18)', padding: 20 }}>
<button onClick={() => setOpen(false)} style={{ position: 'absolute', top: 10, right: 14, background: 'none', border: 'none', cursor: 'pointer', fontSize: 18 }}>×</button>
<h3 style={{ margin: '0 0 12px', fontSize: 14 }}>Send feedback</h3>
{status === 'done' ? (
<p style={{ color: '#10b981', fontSize: 13 }}>Thanks — feedback received.</p>
) : (
<>
<textarea
value={message}
onChange={e => setMessage(e.target.value)}
placeholder="Tell us what you think..."
style={{ width: '100%', boxSizing: 'border-box', minHeight: 80, padding: 10, fontSize: 13, borderRadius: 6, border: '1px solid #e5e7eb', resize: 'vertical' }}
/>
<button
onClick={submit}
disabled={status === 'loading'}
style={{ marginTop: 10, width: '100%', padding: 9, background: '#F59E0B', color: '#000', border: 'none', borderRadius: 6, fontWeight: 600, cursor: 'pointer' }}
>
{status === 'loading' ? 'Sending...' : 'Send'}
</button>
{status === 'error' && <p style={{ color: '#ef4444', fontSize: 12, marginTop: 8 }}>Failed — please try again.</p>}
</>
)}
</div>
)}
</>
);
}

Use it in your layout:

app/layout.tsx
import { FeedbackButton } from '@/components/FeedbackButton';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<FeedbackButton
publishableKey={process.env.NEXT_PUBLIC_NORDVA_KEY!}
userId="user_abc123"
/>
</body>
</html>
);
}

In the Nordva Launch dashboardFeedback, or via the API:

Terminal window
curl "https://api.nordva.dev/v1/feedback?category=bug&status=open" \
-H "Authorization: Bearer nv_live_YOUR_SECRET_KEY"

In the dashboard → FeedbackRouting, connect a destination. All classified feedback matching your filter rules gets forwarded automatically.