Lightweight, customizable survey widgets to collect user feedback in React apps.
react-feedback-surveys is a standalone, open-source (MIT) UI library — no backend, no hosting, no
account required. Render the components, wire onScoreSubmit/onFeedbackSubmit to your own backend
(or nowhere at all).
Want AI-generated insights, a hosted dashboard, and response storage without building your own backend? Check out feedback.tools — built by the same team.
- Introduction
- Features
- Survey Types
- Installation
- Survey Component
- Layout Components
- Props
- Styling
- Demo
- Contributing
- Roadmap
- Changelog
- Credits
- License
- A single
Surveycomponent – the format (methodology, scale length, visual style) is configuration, not a different import - Ready-to-use survey formats – CSAT (2 or 5 points), CES (7 points), NPS (0–10)
- Multiple scale styles – emoji, stars, numbers, thumbs
- Flexible placement – embed inline or display as popup overlay
- Follow-up feedback – optional text input or multiple choice responses
- Optional email collection – close the loop by capturing a respondent's email when no user identity is known
- Optional screenshot attachments – let respondents attach a screenshot, captured by a function you provide
- Fully customizable – CSS variables and custom class names
- Zero dependencies
- TypeScript support
Survey covers four feedback methodologies, selected with the type prop. CSAT is the only one with a variable scale length, set via points:
- CSAT (Customer Satisfaction Score):
type="csat", 2-point or 5-point scale (points={2}orpoints={5}) - NPS (Net Promoter Score):
type="nps", fixed 0–10 scale - CES (Customer Effort Score):
type="ces", fixed 7-point scale - General (text feedback only):
type="general", no rating scale — see General
npm i react-feedback-surveys
# or
yarn add react-feedback-surveysimport 'react-feedback-surveys/index.css';Every format is reached through the same Survey import — type and points pick the methodology and scale length, scaleStyle picks the visual style. See Format Props for the full matrix of valid type/points/scaleStyle combinations.
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 }) => {/* ... */}}
/>points={5} → scaleStyle: emoji | numbers | stars. points={2} → scaleStyle: emoji | thumbs.
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 }) => {/* ... */}}
/>scaleStyle: numbers.
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 }) => {/* ... */}}
/>scaleStyle: numbers.
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' (also accepts 'choices') — it can't be unset, since the text feedback step is the only content the survey has. The step is mandatory: there's no Skip button (textButtonSkipLabel is ignored for type="general"), and Submit stays disabled until there's something to send.
The <Popup> component wraps survey widgets in a fixed overlay that slides in from the screen edge. It includes positioning, animations, and a close button for easy dismissal.
import { Popup, Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
<Popup
animated
classNames={{
base: 'custom-popup-base',
content: 'custom-popup-content',
close: 'custom-popup-close'
}}
placement="bottomRight"
onClose={() => console.log('Closed')}
>
<Survey
type="csat"
points={5}
scaleStyle="stars"
question="How would you rate your satisfaction?"
onScoreSubmit={({ value }) => {/* ... */}}
/>
</Popup>| 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. |
className |
string |
- | - | Additional CSS class name for the popup container. |
classNames |
{ base?: string; content?: string; close?: string } |
- | - | Optional class names for internal popup elements. |
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 (grouped by CSAT/NPS/CES in the sidebar).
Note If the survey inside uses
dir="rtl"on a page that's otherwise LTR (or vice versa), pass the samedirtoPopuptoo — the close button's position resolves against the document direction, while the survey head's reserved offset resolves against the survey's owndir. Passing both keeps them aligned.
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)
- Rounded corners (controlled via
--ft-surface-radius) - Responsive padding that adapts to mobile devices
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?"
onScoreSubmit={({ value }) => {/* ... */}}
/>
</Surface>| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
className |
string |
- | - | Additional CSS class name for the surface container. |
children |
React.ReactNode |
- | - | Content to render inside the surface. |
The Surface component uses the --ft-surface-padding, --ft-surface-padding-mobile, and --ft-surface-radius CSS variables for responsive padding and border radius.
Most props are shared across every survey format. type (and points, for CSAT) select the format; scaleStyle picks its visual style — see Format Props.
| Prop | Type | Required | Description |
|---|---|---|---|
classNames |
ClassNamesConfig (see below) |
- | Optional class names to target internal parts. |
dir |
'ltr' | 'rtl' | 'auto' |
- | Text direction for RTL/LTR language support. |
strings |
SurveyStrings (see below) |
- | Overrides for the library's own aria-labels and other screen-reader-only text. See Accessibility Labels. |
question |
string |
required (not used for type="general") |
Main survey question displayed on the first screen. |
minLabel |
string |
- | Left label for the scale. Not used for type="general". |
maxLabel |
string |
- | Right label for the scale. Not used for type="general". |
getScoreLabelSuffix |
(label: string) => string |
- | Builds the visible text appended after the first/last numbered scale button when it carries minLabel/maxLabel. Default (label) => ` - ${label}`. Applies to the numbers scale style (CSAT5, CES, NPS). |
responseType |
null | 'text' | 'choices' ('text' | 'choices' for type="general", defaults to 'text') |
- | Enables optional follow-up feedback. |
textQuestion |
string |
- | Follow-up question displayed when responseType is defined. |
textButtonSendLabel |
string |
- | Submit label for the feedback screen. |
textButtonSkipLabel |
string |
- | Skip label for the feedback screen. |
choiceOptions |
string[] | null |
- | Predefined choices (when responseType === 'choices'). |
otherPlaceholder |
string |
- | Placeholder for the free-text input next to choice checkboxes. Default 'Other'. |
thankYouMessage |
string |
required | Message shown after submission. |
collectContact |
boolean |
- | Enables an optional email collection step before the success screen. |
userId |
string |
- | Existing user identity. When provided, the email collection step is skipped. |
contactQuestion |
string |
- | Question shown on the email collection screen. |
contactSubtext |
string |
- | Descriptive text shown above the email input. |
contactButtonSendLabel |
string |
- | Submit label for the email collection screen. |
contactButtonSkipLabel |
string |
- | Skip label for the email collection screen. |
onCaptureScreenshot |
() => string | Blob | Promise<string | Blob> |
- | Enables an optional screenshot-attachment control on the feedback step. Hidden unless provided — see Attachments. |
screenshotButtonLabel |
string |
- | Label for the screenshot-attachment control. |
screenshotErrorMessage |
string |
- | Shown to the respondent when onCaptureScreenshot fails. Default 'Failed to capture screenshot'. |
maxAttachments |
number |
- | Maximum number of attachments a respondent may confirm. Default 1. See Attachments. |
attachmentCaption |
string |
- | Visible caption under an attachment thumbnail, also used as its image alt text. Default 'Screenshot'. |
A handful of internal aria-labels ship with an English default. These are never visible UI copy (that's question, thankYouMessage, otherPlaceholder, attachmentCaption, etc., always authored by you, listed alongside the rest of the shared props above) — every key in strings is announced to assistive tech only, useful to override if you're localizing a survey for a non-English audience.
Pass a strings object with only the keys you want to change — each one merges over its own English default, so there's no need to repeat the rest:
<Survey
/* ... */
strings={{
emailLabel: 'Adresse e-mail',
getScoreLabel: (score) => `Score ${score}`
}}
/>| 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' |
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.
interface ClassNamesConfig {
base?: {
base?: string; // The outer widget container
head?: string; // Header row containing title and close button
title?: string; // The heading that shows main/feedback/success text
body?: string; // Main content region (rating scale, feedback form or success)
rating?: string; // Additional class applied when rating screen is active
feedback?: string; // Additional class applied when feedback screen is active
contact?: string; // Additional class applied when email collection screen is active
success?: string; // Additional class applied when success screen is active
close?: string; // Close button
};
scale?: {
base?: string; // Container around the scale style
list?: string; // Wrapper for the interactive items (emoji/stars/numbers)
button?: string; // Each clickable item in the scale
icon?: string; // Icon inside a scale button (emoji, stars)
score?: string; // Number inside a scale button (for numeric variants)
labels?: string; // Left/Right labels displayed under the scale
};
}| Prop | Type | Required | Description |
|---|---|---|---|
onScoreSubmit |
(payload: ScorePayload) => void | Promise<void> |
- | Fires immediately when a score is selected, before any follow-up feedback screen. Captures the raw rating. |
onFeedbackSubmit |
(payload: FeedbackPayload) => void | Promise<void> |
- | Fires when feedback is submitted. Includes the selected score and the user's text(s). |
onContactSubmit |
(payload: ContactPayload) => 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:
type ScorePayload = { value: number };
type FeedbackPayload = { value?: number; text?: string | string[]; attachments?: Attachment[] };
type ContactPayload = { value?: number; text?: string | string[]; email: string };
type Attachment = { kind: 'screenshot'; data: string | Blob; name?: string; mimeType?: string; size?: number };See Attachments.
Event behavior
Invoked immediately when the user selects a score on the rating scale — this callback runs before any optional follow-up screen is shown.
Use it to persist the rating instantly.
The actual value returned depends on the survey type:
- CSAT2:
0–1 - CSAT5:
1–5 - CES7:
1–7 - NPS10:
0–10
Invoked when the user completes the follow-up step and submits their feedback (only applies when responseType is text or choices).
This callback provides both the original score and the user's input.
For surveys with a rating step, the feedback step is optional: respondents can submit feedback or skip it via the textButtonSkipLabel button (or by submitting with empty input), and either action advances to the next screen. For type="general", the feedback step is the survey, so there's no Skip button — Submit stays disabled until there's something to send. onFeedbackSubmit only fires when feedback text or choices are actually submitted; it is not called when a rating-survey's step is skipped.
Arguments:
value?: number— the same score previously passed toonScoreSubmit;undefinedfortype="general", which has no rating steptext: string | string[]— depends onresponseType:text: a single text feedback stringchoices: an array of selected options (may include free-text feedback if enabled)
attachments?: Attachment[]— present when the respondent confirmed a screenshot; see Attachments
Important
You should listen to bothonScoreSubmitandonFeedbackSubmit.
A user may select a score but abandon the follow-up screen (close the widget, navigate away, refresh, etc.).
Handling both events ensures you capture at least the rating even when additional feedback is not provided — and still receive extended data when it is.
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 */}}
/>Respondents can attach a screenshot to their feedback, surfaced on submit as attachments?: Attachment[] (see the event payload types above). Each attachment renders as a thumbnail in the feedback step, with a caption and size underneath, click to open it in a new tab, click the "x" to remove it. Submit is disabled for the brief moment a capture is still in flight, so it can't be confirmed before the attachment actually lands in the list.
Once an attachment is confirmed, the Skip button is disabled — an attached screenshot is never silently discarded. Remove the attachment to re-enable Skip, or fill in feedback and submit to keep it. An attachment on its own is never sufficient to submit, either: for responseType="text", submitting with an attachment but no text flags the textarea instead of sending; for responseType="choices", Submit stays disabled until a choice is picked or text is entered.
By default a respondent can confirm a single attachment: once one is attached, the add control hides. Pass maxAttachments to raise (or lower) that cap. The add control stays visible (below the existing thumbnails) until the cap is reached:
<Survey
/* ... */
onCaptureScreenshot={() => domToDataUrl(document.body)}
maxAttachments={3}
/>Pass onCaptureScreenshot to add an "Capture screenshot" control to the feedback step. Clicking it calls your function and attaches the result immediately. The control is off by default: it doesn't render at all unless onCaptureScreenshot is provided.
Capturing a screenshot needs an actual DOM-to-image library (or a native bridge in a hybrid app) — real, non-trivial code that most consumers of this package won't want to pay for in bundle size if they don't use the feature. So react-feedback-surveys deliberately doesn't ship a capture implementation itself: bring your own function that returns the captured image (as a data URL string, a Blob, or a Promise of either). A drop-in recipe using 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.
| Prop | Type | Required | Description |
|---|---|---|---|
type |
'csat' | 'nps' | 'ces' | 'general' |
required | Survey methodology. Determines the scale range and which scaleStyle/points combinations are valid. 'general' has no scale at all — see General. |
points |
2 | 5 |
required for type="csat"; not used otherwise |
Number of points on the scale. nps is a fixed 0–10 scale and ces a fixed 1–7 scale, so neither takes points. |
Valid scaleStyle values depend on type (and points, for CSAT):
type |
points |
Scale range | Valid scaleStyle |
|---|---|---|---|
csat |
5 |
1–5 | 'emoji' (5 emotion levels) | 'numbers' | 'stars' (1–5 stars) |
csat |
2 |
0–1 | 'emoji' (happy/sad faces) | 'thumbs' (thumbs up/down) |
ces |
— | 1–7 | 'numbers' |
nps |
— | 0–10 | 'numbers' |
Invalid combinations (e.g. type="ces" with scaleStyle="stars") are rejected at the type level.
The package ships with minimal default styles. To use them:
import 'react-feedback-surveys/index.css';You can override colors and fonts via CSS variables:
:root {
/* Main text color for headings and body text */
--ft-color-text: 30 8% 14%;
/* Background color for survey widgets */
--ft-color-bg: 0 0% 100%;
/* Muted text color for labels and secondary content */
--ft-color-muted: 222 11% 46%;
/* Error color for validation messages */
--ft-color-error: 32 95% 44%;
/* Error text color (attachment capture errors) — darker than --ft-color-error, tuned for text contrast rather than borders/outlines */
--ft-color-error-text: 32 95% 32%;
/* Border color for inputs and containers */
--ft-color-border: 214 14% 83%;
/* Outline color for focused interactive elements */
--ft-color-outline: 218 14% 65%;
/* Shadow color for depth and elevation effects */
--ft-color-shadow: 0 0% 0%;
/* Background color for input controls and buttons */
--ft-color-control: 214 20% 96%;
/* Z-index for popup overlay positioning */
--ft-popup-z-index: 49;
/* Inline-end offset for survey head inside popups (right padding in LTR, left in RTL) */
/* Automatically set to 32px inside Popup to prevent title overlap with close button */
/* Set to 0 by default for inline surveys */
--ft-popup-head-offset: 0;
/* Padding for Surface component container (desktop) */
--ft-surface-padding: 20px;
/* Padding for Surface component container on mobile devices (max-width: 400px) */
--ft-surface-padding-mobile: 20px;
/* Border radius for Surface container, inputs, and submit button */
--ft-surface-radius: 8px;
}
/* Use with hsl() function: */
/* color: hsl(var(--ft-color-text)); */
/* background: hsl(var(--ft-color-bg)); */
/* box-shadow: 0 0 10px hsl(var(--ft-color-shadow) / 20%); */The library uses CSS variables for all colors, making it easy to implement custom themes including dark mode. The library itself is theme-agnostic - you control how to override the variables.
Example dark theme color palette:
Here's an example of dark theme colors that work well with the survey components:
/* Example: Class-based dark theme */
.dark {
--ft-color-text: 210 11% 88%;
--ft-color-bg: 220 13% 13%;
--ft-color-muted: 214 10% 60%;
--ft-color-error: 14 90% 62%;
--ft-color-error-text: 14 85% 72%;
--ft-color-border: 217 10% 28%;
--ft-color-outline: 216 12% 45%;
--ft-color-shadow: 0 0% 0%;
--ft-color-control: 218 12% 19%;
}
/* Alternative: Using media query */
@media (prefers-color-scheme: dark) {
:root {
--ft-color-text: 210 11% 88%;
--ft-color-bg: 220 13% 13%;
/* ... other variables */
}
}
/* Alternative: Data attribute based */
[data-theme="dark"] {
--ft-color-text: 210 11% 88%;
--ft-color-bg: 220 13% 13%;
/* ... other variables */
}Implementation example with React:
import { Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
import { useEffect } from 'react';
function App() {
useEffect(() => {
// Example: Apply theme based on system preference
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (isDarkMode) {
document.documentElement.classList.add('dark');
}
// Listen for system preference changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e: MediaQueryListEvent) => {
document.documentElement.classList.toggle('dark', e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
return (
<Survey
type="csat"
points={5}
scaleStyle="emoji"
question="How satisfied are you with our product?"
onScoreSubmit={({ value }) => console.log('Score:', value)}
/>
);
}Manual theme toggle:
// Toggle between light and dark
function toggleTheme() {
document.documentElement.classList.toggle('dark');
}
// Set specific theme
function setTheme(theme: 'light' | 'dark') {
document.documentElement.classList.toggle('dark', theme === 'dark');
}Notes:
- The library only uses CSS variables - how you define them is up to you
- Choose any approach: class-based, data attributes, media queries, or CSS-in-JS
- The example colors provide WCAG AA compliant contrast ratios
- Emoji and icon colors remain unchanged regardless of theme
Or wrap the survey in your own class and target the generated markup.
For deeper customization strategies, see the section below.
Survey accepts a classNames prop with two optional groups: base (outer shell) and scale (the interactive
rating UI). Pass your own class names to override styles without relying on internal selectors.
When is this useful?
- Apply your design system spacing, typography or colors
- Adjust layout (e.g., make the scale full-width, change gaps)
- Restyle scale items (buttons, icons, numbers) consistently
Reference: available keys
| Key | Applies to |
|---|---|
base.base |
The outer widget container |
base.head |
Header row containing title and close button |
base.title |
The heading that shows main/feedback/success text |
base.body |
Main content region (rating scale, feedback form or success) |
base.rating |
Additional class applied when rating screen is active |
base.feedback |
Additional class applied when feedback screen is active |
base.contact |
Additional class applied when email collection screen is active |
base.success |
Additional class applied when success screen is active |
base.close |
Close button |
scale.base |
Container around the scale style |
scale.list |
Wrapper for the interactive items (emoji/stars/numbers) |
scale.button |
Each clickable item in the scale |
scale.icon |
Icon inside a scale button (emoji, stars) |
scale.score |
Number inside a scale button (for numeric variants) |
scale.labels |
Left/Right labels displayed under the scale |
Example: customizing a Survey widget
import { Survey } from 'react-feedback-surveys';
import 'react-feedback-surveys/index.css';
<Survey
classNames={{
base: {
base: 'my-survey-base',
body: 'my-survey-body',
rating: 'my-rating-screen',
feedback: 'my-feedback-screen',
success: 'my-success-screen',
},
scale: {
list: 'my-scale-list',
button: 'my-scale-button',
score: 'my-scale-score',
labels: 'my-scale-labels',
}
}}
type="csat"
points={5}
scaleStyle="numbers"
question="How would you rate your satisfaction with our product?"
minLabel="Very unsatisfied"
maxLabel="Very satisfied"
onScoreSubmit={({ value }) => {/* ... */}}
onFeedbackSubmit={({ value, text }) => {/* ... */}}
/>You can then style these classes in your app stylesheet.
- Live demo: View Storybook
- Run locally:
npm run storybook
npm i
npm run storybookStorybook runs at http://localhost:6006 and is the recommended way to develop and review components.
npm run devThis builds the package to dist/ and watches for changes.
npm run build- Custom emoji & icon support
For a detailed history of changes, see the Changelog.
Emoji icons used in this package are from Sensa Emoji — thanks to the Sensa team for creating such a great set of expressive icons.
MIT © feedback.tools






