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.
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:
<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:
npm install @sensefolks/openfeedbackEmbed OpenFeedback
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 + Nuxt, Angular, Svelte, and Astro examples, see theEmbedding 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 |
thankYouMessage | thank-you-message | string | — | 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 |
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
- Survey Fields (optional) — Ask for name, email, or other configured 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-error-text | #991b1b | Integration error text color |
--sf-error-bg | #fef2f2 | Integration error background |
--sf-error-border | #fecaca | Integration error border color |
--sf-card-bg | #ffffff | Card background color |
--sf-card-border | #d1d5db | Card border color |
--sf-button-text | #ffffff | Button text color |
--sf-card-radius | 8px | Card border radius |
--sf-button-radius | 6px | Button border radius |
--sf-transition | 150ms ease | Transition timing |
--sf-progress-bg | #e5e7eb | Progress track color |
--sf-progress-fill | Primary color | Progress fill color |
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(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
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
| Browser | Version | Notes |
|---|---|---|
| Chrome | 88+ | Full support |
| Firefox | 85+ | Full support |
| Safari | 14+ | Full support |
| Edge | 88+ | Full support |
| IE11 | Not supported | Requires modern Web Component APIs, fetch, and AbortController |