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

typescript
interface Window {
  FeedbackPulse: {
    init: (config: { projectKey: string; apiUrl: string }) => void;
  };
}

Layout: app/layout.tsx

typescript
import FeedbackWidget from '@/components/FeedbackWidget';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <FeedbackWidget />
      </body>
    </html>
  );
}

Environment: .env.local

bash
NEXT_PUBLIC_FEEDBACK_PULSE_KEY=your-project-key
NEXT_PUBLIC_APP_URL=https://pulsefeedback.vercel.app

Next.js (Pages Router)

Custom App: pages/_app.tsx

typescript
import { 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

typescript
import { 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

typescript
import { 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

typescript
import { 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

typescript
import { 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

typescript
import { 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

typescript
FeedbackPulse.init({
  projectKey: string,  // Your project key from dashboard
  apiUrl: string,      // API base URL
});

Full Configuration (Future)

Future releases will support additional options:

typescript
FeedbackPulse.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:

javascript
const 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

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

  1. Load asynchronously
  2. Place script at end of <body>
  3. Use environment variables for keys
  4. 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:

  1. Verify API URL is correct
  2. Check browser console for specific error
  3. Ensure project key is valid

Duplicate Widgets

Avoid initializing multiple times:

typescript
let widgetInitialized = false;

if (!widgetInitialized) {
  FeedbackPulse.init({ ... });
  widgetInitialized = true;
}

Examples

Conditional Loading

Only load on production:

typescript
if (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)

javascript
FeedbackPulse.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