Survey Components
Survey Component
Each format below has a preview and a full example. The valid type/points/scaleStyle combinations are in Survey Types.
CSAT (Customer Satisfaction Score)
Surveys to ask users about their overall satisfaction, or about a specific feature or flow.
Example questions:
- "How satisfied are you with our product?"
- "Was this search helpful?"
- "Are you satisfied with the checkout process?"
import { Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
<Survey
type="csat"
points={5}
scaleStyle="emoji"
question="How would you rate your satisfaction with our product?"
minLabel="Very unsatisfied"
maxLabel="Very satisfied"
responseType="text"
textQuestion="We'd love to hear your thoughts — what can we improve?"
textButtonSendLabel="Send"
textButtonSkipLabel="Skip"
thankYouMessage="Thanks for your feedback!"
onScoreSubmit={({ value }) => {/* ... */}}
onFeedbackSubmit={({ value, text }) => {/* ... */}}
/><Survey
type="csat"
points={2}
scaleStyle="thumbs"
question="Are you satisfied with the result?"
responseType="text"
textQuestion="We'd love to hear your thoughts — what can we improve?"
textButtonSendLabel="Send"
textButtonSkipLabel="Skip"
thankYouMessage="Thank you for your feedback!"
onScoreSubmit={({ value }) => {/* ... */}}
onFeedbackSubmit={({ value, text }) => {/* ... */}}
/>NPS (Net Promoter Score)
Surveys to ask users if they'd recommend your product. Fixed 0–10 scale, no points prop needed.
Example questions:
- "How likely are you to recommend us to a friend or colleague?"
- "On a scale of 0-10, would you recommend our service?"
- "How likely are you to recommend this product to others?"
import { Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
<Survey
type="nps"
scaleStyle="numbers"
question="How likely are you to recommend our product/service to a friend or colleague?"
minLabel="Very unlikely"
maxLabel="Very likely"
responseType="text"
textQuestion="We'd love to hear your thoughts — what can we improve?"
textButtonSendLabel="Send"
textButtonSkipLabel="Skip"
thankYouMessage="Thank you for your feedback!"
onScoreSubmit={({ value }) => {/* ... */}}
onFeedbackSubmit={({ value, text }) => {/* ... */}}
/>CES (Customer Effort Score)
Surveys to ask users how easy it is to use your product. Fixed 7-point scale, no points prop needed.
Example questions:
- "How easy was it to complete your task?"
- "How much effort did it take to resolve your issue?"
- "How easy was it to sign up for an account?"
import { Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
<Survey
type="ces"
scaleStyle="numbers"
question="How easy was it to complete your task?"
minLabel="Very difficult"
maxLabel="Very easy"
responseType="text"
textQuestion="We'd love to hear your thoughts — what can we improve?"
textButtonSendLabel="Send"
textButtonSkipLabel="Skip"
thankYouMessage="Thank you for your feedback!"
onScoreSubmit={({ value }) => {/* ... */}}
onFeedbackSubmit={({ value, text }) => {/* ... */}}
/>General (text feedback only)
No rating scale, just text feedback and an optional screenshot. The text feedback step is shown immediately and is the whole survey; there's no points/scaleStyle/minLabel/maxLabel/question to set.
Example use cases:
- A general "Send feedback" or "Report a problem" trigger with no methodology attached.
- Contexts where a numeric rating doesn't make sense.
import { Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
<Survey
type="general"
textQuestion="Got feedback? We'd love to hear it"
textButtonSendLabel="Send"
thankYouMessage="Thank you for your feedback!"
onFeedbackSubmit={({ text }) => {/* ... */}}
/>responseType defaults to 'text' and also accepts 'choices'. It can't be null, because the feedback step is the whole survey.
The step is mandatory:
- There's no Skip button.
textButtonSkipLabelis ignored. - The respondent must enter text or pick a choice before the survey sends. An attachment alone isn't enough.
Layout Components
Popup
The <Popup> component wraps survey widgets in a fixed overlay pinned to a corner of the screen. It includes positioning, animations, and a close button for easy dismissal.
Usage
import { Popup, Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
<Popup
animated
className="custom-popup"
classNames={{
surface: 'custom-popup-surface',
close: 'custom-popup-close'
}}
placement="bottomRight"
onClose={() => console.log('Closed')}
>
<Survey
type="csat"
points={5}
scaleStyle="stars"
question="How would you rate your satisfaction?"
thankYouMessage="Thanks for your feedback!"
onScoreSubmit={({ value }) => {/* ... */}}
/>
</Popup>Show and Hide
Popup has no open state of its own. It's visible while it's rendered. The close button only calls onClose, so your component decides when to remove the popup:
const [isOpen, setIsOpen] = useState(true);
{isOpen && (
<Popup onClose={() => setIsOpen(false)}>
<Survey /* ... */ />
</Popup>
)}The popup also stays open after the survey shows its success step. To close it after a submit, call setIsOpen(false) in a submit handler, for example after a short delay.
Popup Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
placement | 'topLeft' | 'topRight' | 'bottomRight' | 'bottomLeft' | - | 'bottomRight' | Position of the popup relative to the screen edges. |
animated | boolean | - | true | Enables a fade-in animation when the popup appears. |
theme | 'light' | 'dark' | 'auto' | - | 'light' | Built-in color theme, inherited by the survey inside. See Themes. |
className | string | - | - | Additional CSS class name for the popup container. |
classNames | { surface?: string; close?: string } | - | - | Class names for the card inside the popup and its close button. className styles the popup itself. |
children | React.ReactNode | - | - | Content to render inside the popup (typically a survey component). |
closeLabel | string | - | 'Close survey' | Close button label, used for both its aria-label and title. |
onClose | () => void | - | - | Callback fired when the close button is clicked. |
For more examples, check out the Storybook stories under widgets/Survey (one group per format), and Style Catalog and Styling Cases for customization.
Note If the survey's
dirdiffers from the page direction (for example,dir="rtl"on an LTR page), pass the samedirtoPopup. Otherwise the close button and the space reserved for it in the survey head end up on opposite sides.
Surface
The <Surface> component is a basic container wrapper that provides consistent styling for survey content. It's used internally by the Popup component and can be used standalone to display surveys with a card-like appearance.
The Surface component provides:
- Background color with depth/elevation (box shadow, controlled via
--ft-surface-shadow) - Rounded corners (controlled via
--ft-surface-radius) - Responsive padding that adapts to mobile devices
Usage
import { Surface, Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
<Surface className="custom-surface">
<Survey
type="csat"
points={5}
scaleStyle="stars"
question="How would you rate your satisfaction?"
thankYouMessage="Thanks for your feedback!"
onScoreSubmit={({ value }) => {/* ... */}}
/>
</Surface>Surface Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
className | string | - | - | Additional CSS class name for the surface container. |
theme | 'light' | 'dark' | 'auto' | - | 'light' | Built-in color theme, inherited by the survey inside. See Themes. |
children | React.ReactNode | - | - | Content to render inside the surface. |
The Surface component uses the --ft-surface-padding, --ft-surface-padding-mobile, --ft-surface-radius, and --ft-surface-shadow CSS variables for responsive padding, border radius, and shadow. Inside Popup it uses --ft-popup-shadow instead.
Props
Every survey format takes the same props, except the format props type, points, and scaleStyle (see Survey Types).
Shared Props
A survey shows up to four steps, in this order: rating → feedback → contact → success. The props below are grouped by step. Only two are required: question (except for type="general") and thankYouMessage.
Rating Step
The first screen with the question and the scale. type="general" has no rating step and ignores these props.
| Prop | Type | Required | Description |
|---|---|---|---|
question | string | required | Main survey question. |
minLabel | string | - | Caption at the low end of the scale. |
maxLabel | string | - | Caption at the high end of the scale. |
getScoreLabelSuffix | (label: string) => string | - | Builds the visible text after the first and last numbered buttons from minLabel and maxLabel. Default (label) => ` - ${label}`. Applies to type="ces" and type="nps" only. |
Feedback Step
An optional follow-up question after the rating. It's shown only when responseType is set.
| Prop | Type | Required | Description |
|---|---|---|---|
responseType | null | 'text' | 'choices' | - | 'text' shows a textarea, 'choices' shows checkboxes. null or unset skips the step. For type="general" the default is 'text' and null isn't allowed. |
textQuestion | string | - | Follow-up question. |
choiceOptions | string[] | null | - | Checkbox options for responseType="choices". |
otherPlaceholder | string | - | Placeholder of the free-text input under the checkboxes. Default 'Other'. |
textButtonSendLabel | string | - | Submit button label. |
textButtonSkipLabel | string | - | Skip button label. |
Screenshot Control
A screenshot control on the feedback step. It's shown only when onCaptureScreenshot is set (see Attachments).
| Prop | Type | Required | Description |
|---|---|---|---|
onCaptureScreenshot | () => string | Blob | Promise<string | Blob> | - | Captures the screenshot. |
screenshotButtonLabel | string | - | Accessible name of the screenshot button, which shows only an icon. Default 'Capture screenshot'. |
screenshotErrorMessage | string | - | Shown when onCaptureScreenshot throws or rejects. Default 'Failed to capture screenshot'. |
maxAttachments | number | - | Maximum number of attachments. Default 1. |
attachmentCaption | string | - | Caption under a thumbnail, also its image alt text. Default 'Screenshot'. |
Contact Step
An optional email screen before the success screen. It's shown when collectContact is set and userId isn't (see onContactSubmit).
| Prop | Type | Required | Description |
|---|---|---|---|
collectContact | boolean | - | Enables the step. |
userId | string | - | Known user identity. When set, the step is skipped. |
contactQuestion | string | - | Question on the email screen. |
contactSubtext | string | - | Text above the email input. |
contactButtonSendLabel | string | - | Submit button label. |
contactButtonSkipLabel | string | - | Skip button label. |
Success Step
| Prop | Type | Required | Description |
|---|---|---|---|
thankYouMessage | string | required | Message shown at the end. |
Appearance and Language
These props apply to every step.
| Prop | Type | Required | Description |
|---|---|---|---|
theme | 'light' | 'dark' | 'auto' | - | Built-in color theme. Default 'light'. Set it on the outermost component (Popup or Surface). See Themes. |
classNames | SurveyClassNames | - | Class names for internal parts. See Custom Classes. |
dir | 'ltr' | 'rtl' | 'auto' | - | Text direction. |
strings | SurveyStrings | - | Screen-reader-only labels. See Accessibility Labels. |
Accessibility Labels
strings overrides the library's own aria-labels and other screen-reader-only text. Each key has an English default. Override them when you localize a survey.
Visible text never comes from strings. You set it with regular props, such as question, thankYouMessage, or otherPlaceholder.
Pass only the keys you want to change. Each key merges over its own default:
<Survey
/* ... */
strings={{
emailLabel: 'Adresse e-mail',
getScoreLabel: (score) => `Score ${score}`
}}
/>SurveyStrings Type
| Key | Type | Default |
|---|---|---|
feedbackFormLabel | string | 'Feedback form' |
additionalFeedbackLabel | string | 'Additional feedback' |
yourFeedbackLabel | string | 'Your feedback' |
contactFormLabel | string | 'Contact form' |
emailLabel | string | 'Email address' |
attachmentOpenLabel | string | 'Open screenshot in a new tab' |
attachmentRemoveLabel | string | 'Remove screenshot' |
screenshotProcessingLabel | string | 'Capturing screenshot…' |
screenshotErrorDismissLabel | string | 'Dismiss error' |
getScoreLabel | (score: number) => string | (score) => `Score ${score}` |
getStarsLabel | (score: number) => string | (score) => `${score} ${score > 1 ? 'stars' : 'star'}` |
getScoreLabel applies to the numbers scale style (CSAT5, CES, NPS); getStarsLabel applies to CSAT5's stars style. Both are callbacks rather than templates so you can apply correct pluralization for your target language. Popup's close button label lives on Popup itself, not in strings. See its own closeLabel prop in Popup Props.
Shared Events
| Prop | Type | Required | Description |
|---|---|---|---|
onScoreSubmit | (payload: SurveySubmitPayload) => void | Promise<void> | - | Fires immediately when a score is selected, before any follow-up feedback screen. Captures the raw rating. |
onFeedbackSubmit | (payload: SurveySubmitPayload) => void | Promise<void> | - | Fires when feedback is submitted. Includes the selected score and the user's text(s). |
onContactSubmit | (payload: ContactSubmitPayload) => void | Promise<void> | - | Fires when the respondent submits an email on the optional email collection screen. Not called when the step is skipped or not shown. |
Event Payload Types (exported from the package):
interface SurveySubmitPayload { value?: number; text?: string | string[]; attachments?: SurveyAttachment[] }
interface ContactSubmitPayload { value?: number; text?: string | string[]; email: string }
interface SurveyAttachment { kind: 'screenshot'; data: string | Blob; name?: string; mimeType?: string; size?: number }onScoreSubmit always receives value. text and attachments come with onFeedbackSubmit. See Attachments.
onScoreSubmit
Fires as soon as the respondent picks a score, before the follow-up step. Use it to save the rating right away. The value range depends on the format. See Survey Types.
onFeedbackSubmit
Fires when the respondent submits the feedback step. It receives the score and the respondent's input.
In a survey with a rating, the feedback step is optional:
- The respondent can submit feedback or skip it with the
textButtonSkipLabelbutton. Both go to the next step. - Submit stays disabled until there's text, a choice, or an attachment.
onFeedbackSubmitdoesn't fire when the respondent skips the step.
For type="general" the step is mandatory. See General.
Arguments:
value?: number: the score fromonScoreSubmit.undefinedfortype="general".text?: string | string[]: depends onresponseType.undefinedwhen the respondent submitted only an attachment.text: one feedback string.choices: an array of selected options, plus the free-text input if filled.
attachments?: SurveyAttachment[]: the confirmed screenshots, if any. See Attachments.
Important Listen to both
onScoreSubmitandonFeedbackSubmit. A respondent can pick a score and then leave: close the widget, navigate away, or refresh the page. With both events you always get the rating, and you also get the feedback when the respondent sends it.
onContactSubmit
When collectContact is true and no userId is provided, an optional email collection screen is shown after the rating/feedback screens and before the success screen. This lets you "close the feedback loop" by capturing an email for a respondent whose identity isn't already known to the host app.
- If
userIdis provided, the step is skipped entirely: the widget assumes identity is already known. - The step is always optional: respondents can submit an email or skip it, and either action advances to the success screen.
onContactSubmitonly fires when the respondent actually submits an email; it is not called when the step is skipped or not shown.
Arguments:
value?: number: the selected rating, if any.text?: string | string[]: the submitted feedback text or choices, if any.email: string: the email address entered by the respondent.
<Survey
type="csat"
points={5}
scaleStyle="numbers"
question="How would you rate your satisfaction with our product?"
responseType="text"
textQuestion="We'd love to hear your thoughts — what can we improve?"
thankYouMessage="Thanks for your feedback!"
collectContact
userId={currentUser?.id}
contactQuestion="Mind sharing your email so we can follow up?"
contactButtonSendLabel="Submit"
contactButtonSkipLabel="Skip"
onScoreSubmit={({ value }) => {/* ... */}}
onFeedbackSubmit={({ value, text }) => {/* ... */}}
onContactSubmit={({ value, text, email }) => {/* attribute the feedback to this email */}}
/>Async Handlers
Every event handler can return a Promise. The survey waits for it as follows:
| Handler | While the promise is pending | If the promise rejects |
|---|---|---|
onScoreSubmit | The feedback step opens at once, but its Submit and Skip are disabled. A survey without a feedback step waits on the rating step. | The survey continues as if the promise resolved. |
onFeedbackSubmit | Submit and Skip are disabled. | The survey stays on the feedback step and enables the buttons again, so the respondent can submit again. |
onContactSubmit | Submit and Skip are disabled. | The survey stays on the email step and enables the buttons again. |
The survey never shows an error message for a rejected handler. Reject only when the respondent should submit again. For other errors, retry or log them inside the handler and resolve.
Attachments
Respondents can attach a screenshot to their feedback. onFeedbackSubmit receives it as attachments?: SurveyAttachment[] (see Shared Events).
Pass onCaptureScreenshot to add a screenshot control to the feedback step. Without it, the control doesn't render. A click on the control calls your function and attaches the result.
Each attachment shows as a thumbnail with a caption and its size. A click on the thumbnail opens it in a new tab. The "x" button removes it.
By default a respondent can attach one screenshot, and then the control hides. maxAttachments changes this limit. The control stays in the corner of the text field until the limit is reached:
<Survey
/* ... */
onCaptureScreenshot={() => domToDataUrl(document.body)}
maxAttachments={3}
/>How attachments change the feedback step:
- Skip is disabled while an attachment is attached. This way a screenshot is never discarded by accident. The respondent removes the attachment to skip, or submits to keep it.
- After a rating, an attachment alone is enough to submit.
onFeedbackSubmitthen receivestext: undefined. - In a
generalsurvey, an attachment alone isn't enough. WithresponseType="text", Submit marks the empty textarea as invalid and sends nothing. WithresponseType="choices", Submit stays disabled until the respondent picks a choice or enters text. - Submit is disabled while a capture runs, so the respondent can't send before the screenshot arrives.
While a capture runs, the control shows a spinner. If your function throws or rejects, nothing is attached. The respondent sees screenshotErrorMessage with a button that hides it; the error's own text is never shown. The next capture attempt also hides the message. The library has no timeout for a capture, so add one inside your function if it can hang.
The library doesn't capture screenshots itself. A DOM-to-image library adds a lot to the bundle, and most apps don't need it. Your function returns the image as a data URL string, a Blob, or a Promise of either. A ready recipe with modern-screenshot:
npm i modern-screenshotimport { Survey } from 'react-feedback-surveys';
import { domToDataUrl } from 'modern-screenshot';
import 'react-feedback-surveys/index.css';
<Survey
type="csat"
points={5}
scaleStyle="numbers"
question="How would you rate your satisfaction with our product?"
responseType="text"
textQuestion="We'd love to hear your thoughts — what can we improve?"
thankYouMessage="Thanks for your feedback!"
onCaptureScreenshot={() => domToDataUrl(document.body)}
onScoreSubmit={({ value }) => {/* ... */}}
onFeedbackSubmit={({ value, text, attachments }) => {/* attachments?.[0]?.data holds the attached screenshot */}}
/>In a hybrid app, onCaptureScreenshot can just as well call into a native bridge (e.g. WKWebView.takeSnapshot on iOS) instead of a DOM-to-image library.
Note DOM-to-image capture is best-effort: fonts, cross-origin images, and some CSS effects may not render identically on every browser (particularly Safari/iOS). The respondent can open the attached thumbnail in a new tab to check it, and remove it with one click if the capture came out wrong.