OpenFeedback

OpenFeedback is an embeddable text survey for comments, problems, and explanations.

Place it beside the experience you want feedback on.

Refreshing the page clears unfinished answers.

@sensefolks/openfeedbackWCAG 2.1 AA<10KB gzipped

When should I use OpenFeedback?

Use OpenFeedback for:

  • 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

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

NPM

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-keystring—Required. UUID of the survey from your dashboard
completionMessagecompletion-messagestring—Required. Message displayed after successful submission
thankYouMessagethank-you-messagestring—Overrides the saved survey thank-you message
sessionData—Record<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

The flow can include:

  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

Listen for input, step changes, submission, and errors:

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

Set theme variables on the component 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 exposed parts with ::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);
}

Completion and validation styles

CSS PartElement
success-messageSubmission confirmation
survey-field-error-messageRespondent-field validation feedback

Use --sf-success-bg, --sf-success-border, and --sf-success-text for the confirmation background, border, and text.

Accessibility

OpenFeedback includes:

  • 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