Widget Integration
Detailed guide for integrating the Feedback Pulse widget
Last updated January 29, 2026
Widget Integration Guide
Complete guide for integrating and customizing the Feedback Pulse widget.
Overview
The Feedback Pulse widget is a lightweight, vanilla JavaScript component that adds a floating feedback button to your website. It's framework-agnostic and works with any modern web application.
Features:
- š¦ Zero dependencies - Pure JavaScript
- šØ Auto dark mode - Detects system preference
- āæ Accessible - WCAG compliant
- š± Responsive - Works on all screen sizes
- š Lightweight - < 10KB gzipped
Basic Installation
Script Tag (Recommended)
Add before closing </body> tag:
html<script src="https://pulsefeedback.vercel.app/widget.js"></script> <script> FeedbackPulse.init({ projectKey: 'YOUR_PROJECT_KEY', apiUrl: 'https://pulsefeedback.vercel.app' }); </script>
Async Loading
For better performance, load asynchronously:
html<script> (function() { var script = document.createElement('script'); script.src = 'https://pulsefeedback.vercel.app/widget.js'; script.async = true; script.onload = function() { FeedbackPulse.init({ projectKey: 'YOUR_PROJECT_KEY', apiUrl: 'https://pulsefeedback.vercel.app' }); }; document.body.appendChild(script); })(); </script>
Framework Integration
Next.js (App Router)
Component: src/components/FeedbackWidget.tsx
typescript'use client'; import { useEffect } from 'react'; export default function FeedbackWidget() { useEffect(() => { const script = document.createElement('script'); script.src = 'https://pulsefeedback.vercel.app/widget.js'; script.async = true; script.onload = () => { if (window.FeedbackPulse) { window.FeedbackPulse.init({ projectKey: process.env.NEXT_PUBLIC_FEEDBACK_PULSE_KEY!, apiUrl: process.env.NEXT_PUBLIC_APP_URL!, }); } }; document.body.appendChild(script); return () => { // Cleanup if (document.body.contains(script)) { document.body.removeChild(script); } }; }, []); return null; }
Type Declaration: src/types/feedback-pulse.d.ts
typescriptinterface Window { FeedbackPulse: { init: (config: { projectKey: string; apiUrl: string }) => void; }; }
Layout: app/layout.tsx
typescriptimport FeedbackWidget from '@/components/FeedbackWidget'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <FeedbackWidget /> </body> </html> ); }
Environment: .env.local
bashNEXT_PUBLIC_FEEDBACK_PULSE_KEY=your-project-key NEXT_PUBLIC_APP_URL=https://pulsefeedback.vercel.app
Next.js (Pages Router)
Custom App: pages/_app.tsx
typescriptimport { useEffect } from 'react'; import type { AppProps } from 'next/app'; export default function App({ Component, pageProps }: AppProps) { useEffect(() => { const script = document.createElement('script'); script.src = 'https://pulsefeedback.vercel.app/widget.js'; script.async = true; script.onload = () => { window.FeedbackPulse?.init({ projectKey: process.env.NEXT_PUBLIC_FEEDBACK_PULSE_KEY!, apiUrl: 'https://pulsefeedback.vercel.app', }); }; document.body.appendChild(script); }, []); return <Component {...pageProps} />; }
React (Create React App)
Hook: src/hooks/useFeedbackWidget.ts
typescriptimport { useEffect } from 'react'; export function useFeedbackWidget() { useEffect(() => { const script = document.createElement('script'); script.src = 'https://pulsefeedback.vercel.app/widget.js'; script.async = true; script.onload = () => { if (window.FeedbackPulse) { window.FeedbackPulse.init({ projectKey: process.env.REACT_APP_FEEDBACK_KEY!, apiUrl: 'https://pulsefeedback.vercel.app', }); } }; document.body.appendChild(script); return () => { if (document.body.contains(script)) { document.body.removeChild(script); } }; }, []); }
App: src/App.tsx
typescriptimport { useFeedbackWidget } from './hooks/useFeedbackWidget'; function App() { useFeedbackWidget(); return ( <div className="App"> {/* Your app */} </div> ); } export default App;
Vue.js 3
Composable: src/composables/useFeedbackWidget.ts
typescriptimport { onMounted, onUnmounted } from 'vue'; export function useFeedbackWidget() { let script: HTMLScriptElement; onMounted(() => { script = document.createElement('script'); script.src = 'https://pulsefeedback.vercel.app/widget.js'; script.async = true; script.onload = () => { window.FeedbackPulse?.init({ projectKey: import.meta.env.VITE_FEEDBACK_KEY, apiUrl: 'https://pulsefeedback.vercel.app', }); }; document.body.appendChild(script); }); onUnmounted(() => { if (script && document.body.contains(script)) { document.body.removeChild(script); } }); }
App: src/App.vue
vue<template> <router-view /> </template> <script setup lang="ts"> import { useFeedbackWidget } from './composables/useFeedbackWidget'; useFeedbackWidget(); </script>
Angular
Service: src/app/services/feedback-widget.service.ts
typescriptimport { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class FeedbackWidgetService { private script?: HTMLScriptElement; loadWidget(): void { this.script = document.createElement('script'); this.script.src = 'https://pulsefeedback.vercel.app/widget.js'; this.script.async = true; this.script.onload = () => { (window as any).FeedbackPulse?.init({ projectKey: 'YOUR_PROJECT_KEY', apiUrl: 'https://pulsefeedback.vercel.app', }); }; document.body.appendChild(this.script); } removeWidget(): void { if (this.script && document.body.contains(this.script)) { document.body.removeChild(this.script); } } }
Component: src/app/app.component.ts
typescriptimport { Component, OnInit, OnDestroy } from '@angular/core'; import { FeedbackWidgetService } from './services/feedback-widget.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit, OnDestroy { constructor(private feedbackWidget: FeedbackWidgetService) {} ngOnInit(): void { this.feedbackWidget.loadWidget(); } ngOnDestroy(): void { this.feedbackWidget.removeWidget(); } }
Configuration Options
Required Options
typescriptFeedbackPulse.init({ projectKey: string, // Your project key from dashboard apiUrl: string, // API base URL });
Full Configuration (Future)
Future releases will support additional options:
typescriptFeedbackPulse.init({ projectKey: 'abc123', apiUrl: 'https://pulsefeedback.vercel.app', // Position (planned) position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left', // Colors (planned) theme: { primaryColor: '#000000', backgroundColor: '#ffffff', }, // Text (planned) labels: { buttonText: 'Feedback', title: 'Send Feedback', placeholder: 'Tell us what you think...', }, // Behavior (planned) autoShow: boolean, showDelay: number, });
Widget Anatomy
Floating Button
āāāāāāāāāāāāāāāāāāāāāāāāāā
ā š¬ Feedback ā ā Pill-shaped button
āāāāāāāāāāāāāāāāāāāāāāāāāā
Styling:
- Position: Fixed, bottom-right
- Background: Adapts to theme (light/dark)
- Animation: Subtle pulse effect
- Z-index: 9999
Modal
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā ā Send Feedback ā ā Header with close button
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā [ Bug ] [ Feature ] [ Other ] ā ā Tab selector
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā What's on your mind? ā ā ā Message textarea
ā ā ā ā
ā ā ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Name (optional) [__________] ā ā Optional fields
ā Email (optional) [__________] ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā [ Submit Feedback ] ā ā Submit button
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Features:
- Keyboard accessible (Tab, Enter, Esc)
- Click outside to close
- Form validation
- Loading state on submit
- Success/error messages
Dark Mode
The widget automatically detects system dark mode preference:
javascriptconst isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
Light Mode:
- White background
- Black text
- Light borders
Dark Mode:
- Dark gray background (#1a1a1a)
- White text
- Subtle borders
Accessibility
The widget follows WCAG 2.1 AA guidelines:
Keyboard Navigation
- Tab - Navigate between fields
- Enter - Submit form / Select tab
- Escape - Close modal
- Arrow keys - Navigate tabs
ARIA Labels
html<button aria-label="Open feedback form">Feedback</button> <dialog role="dialog" aria-labelledby="feedback-title"> <h2 id="feedback-title">Send Feedback</h2> </dialog>
Focus Management
- Focus trapped in modal when open
- Returns to trigger button on close
- Visible focus indicators
Screen Readers
- Descriptive labels for all inputs
- Error messages announced
- Success confirmation announced
API Communication
Request Flow
User submits ā Widget validates ā POST /api/widget/[key] ā Background label generation ā Response
Request Format
typescriptPOST /api/widget/abc123 Content-Type: application/json { "type": "bug", "message": "Login button is broken", "email": "user@example.com", "name": "John Doe" }
Response
Success:
json{ "success": true }
Error:
json{ "error": "Invalid project key" }
Error Handling
The widget handles:
- ā Network errors (timeout, offline)
- ā Server errors (500)
- ā Invalid project key (404)
- ā Validation errors (400)
All errors show user-friendly messages in the modal.
Performance
Bundle Size
- Uncompressed: ~15KB
- Gzipped: ~8KB
- Load time: < 100ms on 3G
Best Practices
- Load asynchronously
- Place script at end of
<body> - Use environment variables for keys
- Enable compression on server
Troubleshooting
Widget Not Loading
javascript// Check if script loaded console.log(window.FeedbackPulse); // Should not be undefined // Check for errors window.addEventListener('error', (e) => { if (e.filename?.includes('widget.js')) { console.error('Widget failed to load:', e); } });
CORS Errors
Widget API allows cross-origin requests. If you see CORS errors:
- Verify API URL is correct
- Check browser console for specific error
- Ensure project key is valid
Duplicate Widgets
Avoid initializing multiple times:
typescriptlet widgetInitialized = false; if (!widgetInitialized) { FeedbackPulse.init({ ... }); widgetInitialized = true; }
Examples
Conditional Loading
Only load on production:
typescriptif (process.env.NODE_ENV === 'production') { FeedbackPulse.init({ ... }); }
Custom Trigger
Hide default button and use custom trigger:
html<!-- Note: Currently not supported, planned for future --> <button onclick="FeedbackPulse.open()"> Give Feedback </button> <script> FeedbackPulse.init({ projectKey: 'abc123', apiUrl: 'https://pulsefeedback.vercel.app', hideButton: true // Planned feature }); </script>
Event Listeners (Planned)
javascriptFeedbackPulse.on('submit', (data) => { console.log('Feedback submitted:', data); // Track with analytics }); FeedbackPulse.on('error', (error) => { console.error('Feedback error:', error); });
Security
API Key Protection
Public exposure is safe:
- Project keys are designed to be public
- They only allow feedback submission
- Cannot read or modify existing data
- Rate limiting planned for abuse prevention
Best practices:
- Use environment variables
- Rotate keys if compromised
- Monitor feedback volume
Data Privacy
- Widget only sends data you provide
- No tracking or analytics by default
- HTTPS enforced in production
- User email/name optional
Support
- Issues: GitHub Issues
- Email: sarbosarcar@gmail.com
- Docs: Documentation Home