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.
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:
<!-- 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:
npm install @sensefolks/openfeedback Framework Integration
HTML / Vanilla JS
<sf-openfeedback
survey-key="your-survey-uuid"
completion-message="Thank you for your feedback!"
enable-events="true">
</sf-openfeedback> React
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:
- Question Step — Open-ended textarea for feedback
- Respondent Details (optional) — Collect name, email, or custom fields
- 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
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
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 | #005fcc | Primary brand color |
--sf-primary-hover | #0047a3 | Primary hover color |
--sf-text-primary | #111827 | Primary text color |
--sf-text-secondary | #6b7280 | Secondary/muted text color |
--sf-error-color | #dc2626 | Error state color |
--sf-card-bg | #ffffff | Card background color |
--sf-card-border | #d1d5db | Card border color |
--sf-card-radius | 8px | Card border radius |
--sf-button-radius | 6px | Button border radius |
--sf-transition | 150ms ease | Transition timing |
CSS Parts
Style the component externally using ::part():
/* 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
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 |
|---|---|---|
| Chrome | 88+ | Full support |
| Firefox | 85+ | Full support |
| Safari | 14+ | Full support |
| Edge | 88+ | Full support |
| IE11 | Supported | ES5 build with polyfills |