OpenFeedback

OpenFeedback gives people a text box for the explanation, need, or issue that fixed answer options cannot capture.

Place it where the experience is still fresh. A structured survey can show what happened; OpenFeedback can help you understand why.

Draft feedback stays only in the mounted component. Reloading the page starts a new blank response.

@sensefolks/openfeedbackWCAG 2.1 AA<10KB gzipped

When OpenFeedback Is the Right Survey

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
<script type="module" src="https://unpkg.com/@sensefolks/[email protected]/dist/sf-openfeedback/sf-openfeedback.esm.js"></script>

NPM

For projects with a build system:

bash
npm install @sensefolks/openfeedback

Embed OpenFeedback

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 + Nuxt, Angular, Svelte, and Astro examples, see theEmbedding Tutorial.

API Reference

Properties

PropertyAttributeTypeDefaultDescription
surveyKeysurvey-keystringRequired. UUID of the survey from your dashboard
completionMessagecompletion-messagestringRequired. Message displayed after successful submission
thankYouMessagethank-you-messagestringOverrides the saved survey thank-you message
sessionDataRecord<string, string | number | boolean>{}Complete typed Session Data matching every field in the schema defined during survey creation
enableEventsenable-eventsbooleantrueWhether 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. Survey Fields (optional) — Ask for name, email, or other configured fields
  3. Completion — Thank you message

Custom Events

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

Events Reference

EventDescriptionDetail Properties
sfReadySurvey loaded and ready to displaysurveyKey, question
sfInputUser typed in the feedback textareasurveyKey, value, characterCount
sfStepChangeUser navigated between survey stepssurveyKey, previousStep, currentStep, currentStepIndex
sfSubmitSurvey submitted successfullysurveyKey, feedback, completionTimeSeconds
sfErrorError 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:

PropertyDefaultDescription
--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-error-text#991b1bIntegration error text color
--sf-error-bg#fef2f2Integration error background
--sf-error-border#fecacaIntegration error border color
--sf-card-bg#ffffffCard background color
--sf-card-border#d1d5dbCard border color
--sf-button-text#ffffffButton text color
--sf-card-radius8pxCard border radius
--sf-button-radius6pxButton border radius
--sf-transition150ms easeTransition timing
--sf-progress-bg#e5e7ebProgress track color
--sf-progress-fillPrimary colorProgress fill color

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(survey-fields-step) { }
sf-openfeedback::part(completion-step) { }

/* Progress */
sf-openfeedback::part(progress) { }
sf-openfeedback::part(progress-indicator) { }
sf-openfeedback::part(progress-label) { }
sf-openfeedback::part(progress-text) { }
sf-openfeedback::part(progress-track) { }
sf-openfeedback::part(progress-bar) { }
sf-openfeedback::part(progress-fill) { }

/* Headings */
sf-openfeedback::part(heading) { }
sf-openfeedback::part(question-heading) { }
sf-openfeedback::part(survey-fields-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(survey-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) { }

/* Messages & States */
sf-openfeedback::part(message) { }
sf-openfeedback::part(error-message) { }
sf-openfeedback::part(loading-message) { }
sf-openfeedback::part(error-container) { }
sf-openfeedback::part(troubleshooting-link) { }
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: 400;
  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

BrowserVersionNotes
Chrome88+Full support
Firefox85+Full support
Safari14+Full support
Edge88+Full support
IE11Not supportedRequires modern Web Component APIs, fetch, and AbortController