Jump to

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?"
How would you rate your satisfaction with our product?
Very dissatisfied
Very satisfied
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 }) => {/* ... */}}
/>
Are you satisfied with the result?
<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?"
How likely are you to recommend our product/service to a friend or colleague?
0
1
2
3
4
5
6
7
8
9
10
Very unlikely
Very likely
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?"
How easy was it to complete your task?
1
2
3
4
5
6
7
Very difficult
Very easy
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. textButtonSkipLabel is ignored.
  • The respondent must enter text or pick a choice before the survey sends. An attachment alone isn't enough.

Layout Components

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.

How would you rate your satisfaction with our product?
Very dissatisfied
Very satisfied

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.

Comparison table
PropTypeRequiredDefaultDescription
placement'topLeft' | 'topRight' | 'bottomRight' | 'bottomLeft'-'bottomRight'Position of the popup relative to the screen edges.
animatedboolean-trueEnables a fade-in animation when the popup appears.
theme'light' | 'dark' | 'auto'-'light'Built-in color theme, inherited by the survey inside. See Themes.
classNamestring--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.
childrenReact.ReactNode--Content to render inside the popup (typically a survey component).
closeLabelstring-'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 dir differs from the page direction (for example, dir="rtl" on an LTR page), pass the same dir to Popup. 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

Comparison table
PropTypeRequiredDefaultDescription
classNamestring--Additional CSS class name for the surface container.
theme'light' | 'dark' | 'auto'-'light'Built-in color theme, inherited by the survey inside. See Themes.
childrenReact.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.

Comparison table
PropTypeRequiredDescription
questionstringrequiredMain survey question.
minLabelstring-Caption at the low end of the scale.
maxLabelstring-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.

Comparison table
PropTypeRequiredDescription
responseTypenull | '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.
textQuestionstring-Follow-up question.
choiceOptionsstring[] | null-Checkbox options for responseType="choices".
otherPlaceholderstring-Placeholder of the free-text input under the checkboxes. Default 'Other'.
textButtonSendLabelstring-Submit button label.
textButtonSkipLabelstring-Skip button label.

Screenshot Control

A screenshot control on the feedback step. It's shown only when onCaptureScreenshot is set (see Attachments).

Comparison table
PropTypeRequiredDescription
onCaptureScreenshot() => string | Blob | Promise<string | Blob>-Captures the screenshot.
screenshotButtonLabelstring-Accessible name of the screenshot button, which shows only an icon. Default 'Capture screenshot'.
screenshotErrorMessagestring-Shown when onCaptureScreenshot throws or rejects. Default 'Failed to capture screenshot'.
maxAttachmentsnumber-Maximum number of attachments. Default 1.
attachmentCaptionstring-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).

Comparison table
PropTypeRequiredDescription
collectContactboolean-Enables the step.
userIdstring-Known user identity. When set, the step is skipped.
contactQuestionstring-Question on the email screen.
contactSubtextstring-Text above the email input.
contactButtonSendLabelstring-Submit button label.
contactButtonSkipLabelstring-Skip button label.

Success Step

Comparison table
PropTypeRequiredDescription
thankYouMessagestringrequiredMessage shown at the end.

Appearance and Language

These props apply to every step.

Comparison table
PropTypeRequiredDescription
theme'light' | 'dark' | 'auto'-Built-in color theme. Default 'light'. Set it on the outermost component (Popup or Surface). See Themes.
classNamesSurveyClassNames-Class names for internal parts. See Custom Classes.
dir'ltr' | 'rtl' | 'auto'-Text direction.
stringsSurveyStrings-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

Comparison table
KeyTypeDefault
feedbackFormLabelstring'Feedback form'
additionalFeedbackLabelstring'Additional feedback'
yourFeedbackLabelstring'Your feedback'
contactFormLabelstring'Contact form'
emailLabelstring'Email address'
attachmentOpenLabelstring'Open screenshot in a new tab'
attachmentRemoveLabelstring'Remove screenshot'
screenshotProcessingLabelstring'Capturing screenshot…'
screenshotErrorDismissLabelstring'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

Comparison table
PropTypeRequiredDescription
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 textButtonSkipLabel button. Both go to the next step.
  • Submit stays disabled until there's text, a choice, or an attachment.
  • onFeedbackSubmit doesn't fire when the respondent skips the step.

For type="general" the step is mandatory. See General.

Arguments:

  • value?: number: the score from onScoreSubmit. undefined for type="general".
  • text?: string | string[]: depends on responseType. undefined when 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 onScoreSubmit and onFeedbackSubmit. 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 userId is 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.
  • onContactSubmit only 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:

Comparison table
HandlerWhile the promise is pendingIf the promise rejects
onScoreSubmitThe 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.
onFeedbackSubmitSubmit and Skip are disabled.The survey stays on the feedback step and enables the buttons again, so the respondent can submit again.
onContactSubmitSubmit 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. onFeedbackSubmit then receives text: undefined.
  • In a general survey, an attachment alone isn't enough. With responseType="text", Submit marks the empty textarea as invalid and sends nothing. With responseType="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-screenshot
import { 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.