OpenFeedback

OpenFeedback collects qualitative input in users' own words so teams can understand context behind behavior and metrics.

Embed it at moments where users can explain what happened and why. Structured surveys quantify outcomes; OpenFeedback adds context.

@sensefolks/openfeedback WCAG 2.1 AA <10KB gzipped

When to Use OpenFeedback

Embed OpenFeedback at moments where users are likely to share context:

  • Help docs and FAQs — "Didn't find what you needed?"
  • Error states — "What were you trying to do?"
  • Exit intent modals — "What made you leave?"
  • Account cancellation flows — "How can we improve?"
  • Feature request pages — "What would you like to see?"
  • Bug report forms — "Describe what happened"

Installation

CDN (Recommended)

Add directly to your HTML:

html
<!-- Modern browsers (ES modules) -->
<script type="module" src="https://unpkg.com/@sensefolks/openfeedback/dist/sf-openfeedback/sf-openfeedback.esm.js"></script>

<!-- Legacy browsers -->
<script nomodule src="https://unpkg.com/@sensefolks/openfeedback/dist/sf-openfeedback/sf-openfeedback.js"></script>

NPM

For projects with a build system:

bash
npm install @sensefolks/openfeedback

Framework Integration

HTML / Vanilla JS

html
<sf-openfeedback 
  survey-key="your-survey-uuid" 
  completion-message="Thank you for your feedback!"
  enable-events="true">
</sf-openfeedback>

React

jsx
import { useEffect } from 'react';

function FeedbackForm({ surveyKey }) {
  useEffect(() => {
    import('@sensefolks/openfeedback');
  }, []);

  return (
    <sf-openfeedback 
      survey-key={surveyKey}
      completion-message="Thank you for your feedback!">
    </sf-openfeedback>
  );
}

For Vue, Angular, Next.js, and Svelte examples, see the Embedding Tutorial.

API Reference

Properties

Property Attribute Type Default Description
surveyKey survey-key string Required. UUID of the survey from your dashboard
completionMessage completion-message string Required. Message displayed after successful submission
enableEvents enable-events boolean true Whether to emit custom events (sfReady, sfStepChange, sfInput, sfSubmit, sfError)

Survey Flow

OpenFeedback supports a multi-step flow:

  1. Question Step — Open-ended textarea for feedback
  2. Respondent Details (optional) — Collect name, email, or custom fields
  3. Completion — Thank you message

Custom Events

OpenFeedback emits custom events for tracking user interactions and integrating with analytics.

Events Reference

Event Description Detail Properties
sfReady Survey loaded and ready to display surveyKey, question
sfInput User typed in the feedback textarea surveyKey, value, characterCount
sfStepChange User navigated between survey steps surveyKey, previousStep, currentStep, currentStepIndex
sfSubmit Survey submitted successfully surveyKey, feedback, completionTimeSeconds
sfError Error occurred (load, submit, or validation) surveyKey, errorType, errorMessage

Vanilla JavaScript

javascript
const feedback = document.querySelector('sf-openfeedback');

// Survey loaded and ready
feedback.addEventListener('sfReady', (e) => {
  console.log('Survey ready:', e.detail);
  // { surveyKey, question }
});

// User typing in the textarea
feedback.addEventListener('sfInput', (e) => {
  console.log('User input:', e.detail);
  // { surveyKey, value, characterCount }
});

// User navigated between steps
feedback.addEventListener('sfStepChange', (e) => {
  console.log('Step changed:', e.detail);
  // { surveyKey, previousStep, currentStep, currentStepIndex }
});

// Survey submitted successfully
feedback.addEventListener('sfSubmit', (e) => {
  console.log('Survey submitted:', e.detail);
  // { surveyKey, feedback, completionTimeSeconds }
});

// Error occurred
feedback.addEventListener('sfError', (e) => {
  console.error('Survey error:', e.detail);
  // { surveyKey, errorType, errorMessage }
});

React

jsx
import { useEffect, useRef } from 'react';

function FeedbackWithEvents({ surveyKey, onSubmit }) {
  const feedbackRef = useRef(null);

  useEffect(() => {
    import('@sensefolks/openfeedback');
    
    const el = feedbackRef.current;
    if (!el) return;

    const handleSubmit = (e) => {
      console.log('Feedback submitted:', e.detail.feedback);
      onSubmit?.(e.detail);
    };

    const handleInput = (e) => {
      // Track engagement - user started typing
      if (e.detail.characterCount === 1) {
        analytics.track('feedback_started');
      }
    };

    el.addEventListener('sfSubmit', handleSubmit);
    el.addEventListener('sfInput', handleInput);

    return () => {
      el.removeEventListener('sfSubmit', handleSubmit);
      el.removeEventListener('sfInput', handleInput);
    };
  }, [onSubmit]);

  return (
    <sf-openfeedback 
      ref={feedbackRef}
      survey-key={surveyKey}
      completion-message="Thanks for your feedback!">
    </sf-openfeedback>
  );
}

Styling

CSS Custom Properties

Customize the component by setting CSS custom properties on the element or a parent:

Property Default Description
--sf-primary#005fccPrimary brand color
--sf-primary-hover#0047a3Primary hover color
--sf-text-primary#111827Primary text color
--sf-text-secondary#6b7280Secondary/muted text color
--sf-error-color#dc2626Error state color
--sf-card-bg#ffffffCard background color
--sf-card-border#d1d5dbCard border color
--sf-card-radius8pxCard border radius
--sf-button-radius6pxButton border radius
--sf-transition150ms easeTransition timing

CSS Parts

Style the component externally using ::part():

css
/* Container & Layout */
sf-openfeedback::part(container) { }
sf-openfeedback::part(step) { }
sf-openfeedback::part(question-step) { }
sf-openfeedback::part(respondent-details-step) { }
sf-openfeedback::part(completion-step) { }

/* Headings */
sf-openfeedback::part(heading) { }
sf-openfeedback::part(question-heading) { }
sf-openfeedback::part(respondent-heading) { }
sf-openfeedback::part(completion-heading) { }

/* Textarea */
sf-openfeedback::part(input) { }
sf-openfeedback::part(textarea) { }
sf-openfeedback::part(form-textarea) { }

/* Form Fields */
sf-openfeedback::part(respondent-fields-container) { }
sf-openfeedback::part(form-container) { }
sf-openfeedback::part(field) { }
sf-openfeedback::part(form-field) { }
sf-openfeedback::part(field-error) { }
sf-openfeedback::part(field-label) { }
sf-openfeedback::part(form-label) { }
sf-openfeedback::part(form-input) { }
sf-openfeedback::part(select) { }
sf-openfeedback::part(form-select) { }
sf-openfeedback::part(hcaptcha-container) { }
sf-openfeedback::part(required-indicator) { }

/* Radio & Checkbox Groups */
sf-openfeedback::part(radio-group) { }
sf-openfeedback::part(radio-option) { }
sf-openfeedback::part(radio-input) { }
sf-openfeedback::part(radio-label) { }
sf-openfeedback::part(checkbox-group) { }
sf-openfeedback::part(checkbox-option) { }
sf-openfeedback::part(checkbox-input) { }
sf-openfeedback::part(checkbox-label) { }

/* Buttons */
sf-openfeedback::part(button-container) { }
sf-openfeedback::part(button) { }
sf-openfeedback::part(next-button) { }
sf-openfeedback::part(back-button) { }
sf-openfeedback::part(submit-button) { }
sf-openfeedback::part(retry-button) { }

/* Messages & States */
sf-openfeedback::part(message) { }
sf-openfeedback::part(error-message) { }
sf-openfeedback::part(loading-message) { }
sf-openfeedback::part(error-container) { }
sf-openfeedback::part(empty-message) { }
sf-openfeedback::part(announcements) { }

/* Branding */
sf-openfeedback::part(branding) { }
sf-openfeedback::part(branding-link) { }
sf-openfeedback::part(branding-logo) { }

Styling Example

css
sf-openfeedback {
  --sf-primary: #7c3aed;
  --sf-error-color: #e11d48;
  --sf-card-radius: 12px;
  --sf-button-radius: 8px;
}

sf-openfeedback::part(textarea) {
  border: 1px solid var(--sf-card-border);
  border-radius: var(--sf-card-radius);
  padding: 0.75rem;
  font-size: 1rem;
}

sf-openfeedback::part(next-button) {
  background-color: var(--sf-primary);
  color: #ffffff;
  border-radius: var(--sf-button-radius);
  padding: 0.5rem 1.5rem;
}

sf-openfeedback::part(field-label) {
  font-weight: 600;
  color: var(--sf-text-primary);
}

Accessibility

OpenFeedback is built with accessibility as a core feature:

  • Keyboard Navigation — Tab between fields, Enter to submit
  • Screen Readers — ARIA labels, live regions for announcements
  • Form Validation — Accessible error messages with aria-invalid
  • Focus Management — Visible focus indicators, logical tab order
  • High Contrast — Works with Windows High Contrast Mode
  • Reduced Motion — Respects prefers-reduced-motion

Troubleshooting

Textarea not accepting input

  • Check browser console for JavaScript errors
  • Make sure the survey key is valid and the survey is published

Events not firing

  • Listen on the component element itself, not a wrapper
  • Check that the component has loaded (wait for the sfReady event)

Styles not applying

  • Use ::part() selectors. Regular CSS won't penetrate Shadow DOM
  • Check the CSS Parts reference for correct part names

Browser Support

Browser Version Notes
Chrome88+Full support
Firefox85+Full support
Safari14+Full support
Edge88+Full support
IE11 SupportedES5 build with polyfills