NPS Survey Card
An NPS survey card for React: 0-10 segmented buttons coloured by detractor/passive/promoter band, a follow-up question that expands after scoring, a thank-you state, plus npsCategory and npsScore helpers.
The classic SaaS satisfaction survey. Eleven score buttons (0-10) are split into the three NPS bands — 0-6 detractors (red), 7-8 passives (amber), 9-10 promoters (green). Picking a score tints the whole band, pops the selected button, and expands a follow-up question that changes with the score (low scores ask what went wrong, high scores ask what you liked most). Submitting flips the whole card into a check-mark thank-you state. The pure helpers npsCategory and npsScore are exported too, so your backend can turn collected scores into an NPS figure with the exact same rules.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add nps-survey-card.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<NpsSurveyCard/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/nps-survey-card.en.jsonOr, once registries are configured in components.json, install it as @webberui/nps-survey-card.
Usage
import { NpsSurveyCard, npsScore } from "@/components/ui/nps-survey-card";
<NpsSurveyCard
onChange={(score) => console.log("picked", score)}
onSubmit={({ score, comment }) => console.log(score, comment)}
requireComment
/>
// The score maths can be used on its own: promoter% − detractor%, an integer from -100 to 100
npsScore([10, 9, 8, 6, 3]); // 40 − 40 = 0Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | null | — | Controlled selected score (0-10); null means nothing picked yet, and omitting the prop makes the component uncontrolled |
defaultValue | number | null | null | Initial score in uncontrolled mode |
onChange | (score: number) => void | — | Callback when a score is picked (click, arrow keys and number keys all fire it) |
onSubmit | (result: { score: number; comment: string }) => void | — | Callback on submit, with the score and the trimmed follow-up comment |
onReset | () => void | — | When provided, the thank-you state shows a "fill in again" button that resets internal state and calls back |
question | string | "How likely are you to recommend us to a friend or colleague?" | Main question text (default is Chinese for "How likely are you to recommend us to a friend or colleague?") |
followUpQuestion | string | — | Follow-up template; {score} in the template is replaced with the actual score, and it takes precedence over followUps and the built-in defaults |
followUps | Partial<Record<NpsCategory, string>> | — | Per-band follow-up overrides (detractor/passive/promoter), also supporting the {score} placeholder |
showLabels | boolean | true | Show the two end labels ("not at all likely" / "extremely likely") |
requireComment | boolean | false | Make the comment mandatory: the submit button stays disabled while it is empty |
compact | boolean | false | Compact mode: smaller buttons, type size and spacing |
labels | Partial<NpsSurveyCardLabels> | — | Override the interface copy |
className | string | — | Forwarded to the outermost card |
NpsSurveyCardLabels
| Field | Type | Default | Description |
|---|---|---|---|
low | string | "Absolutely Impossible" | Label at the 0 end (default is Chinese for "not at all likely") |
high | string | "Very Likely" | Label at the 10 end (default is Chinese for "extremely likely") |
detractor | string | "Detractor" | Name of the 0-6 band (used in accessibility announcements; Chinese for "detractor") |
passive | string | "Passive" | Name of the 7-8 band (Chinese for "passive") |
promoter | string | "Recommender" | Name of the 9-10 band (Chinese for "promoter") |
commentPlaceholder | string | "Write Anything You Think Of" | Placeholder of the follow-up textarea (Chinese for "write whatever comes to mind") |
commentRequired | string | "This field is required; just write a few words." | Hint shown when requireComment is on (Chinese for "this field is required — a few words will do") |
submit | string | "Send" | Submit button text (Chinese for "submit") |
thanksTitle | string | "Thank you for your feedback!" | Thank-you title (Chinese for "thanks for your feedback!") |
thanksBody | string | "We will carefully read every comment." | Thank-you body copy (Chinese for "we read every piece of feedback") |
reset | string | "Enter Again" | Text of the reset button (Chinese for "fill in again"; only shown when onReset is provided) |
Also exported by name: npsCategory(score) (buckets a 0-10 score into "detractor" | "passive" | "promoter"), npsScore(scores) (promoter percentage minus detractor percentage, rounded to an integer; an empty array returns 0), and the NpsCategory and NpsSurveyCardLabels types, so your backend can reuse the exact same scoring rules.
How it works
- Follow-up text resolution order:
followUpQuestion(with{score}interpolated) wins, then the per-bandfollowUpsoverride, then the built-in defaults — low scores ask what disappointed you, mid scores ask what is missing for a higher score, high scores ask what you liked most - Enter inside the follow-up textarea only inserts a newline and never submits — submission always goes through the button, so nothing is sent mid-thought
- In narrow containers the 11 buttons wrap onto two rows automatically (flex-wrap), with no extra configuration
- The "fill in again" button only appears when
onResetis provided; in controlled mode, setvalueback tonullinside youronResetcallback
Accessibility
- The 0-10 buttons form a
role="radiogroup"(named by the main question viaaria-labelledby); each score is arole="radio"witharia-checked, using a roving tabindex — Tab into the group, then move the selection with ←/→ (or number keys and Home/End) - Picking a score is announced through an
aria-live="polite"region together with its band (detractor/passive/promoter), and submitting announces the thank-you message - The follow-up textarea is associated with its question via
label; withrequireCommenton,aria-requiredis set andaria-describedbypoints at the hint - When the user has "reduce motion" enabled at the system level, the selection pop, hover lift, expand and check-mark animations are disabled and states switch instantly
Quantity Stepper
A React ± quantity stepper for ecommerce: press-and-hold acceleration, min/max with an at-limit shake, direct typing, a trash icon at zero, and rolling digits, in compact and pill variants.
Select
A button-triggered select whose value crossfades on change, with an anchored list that flips at edges, groups and separators, typeahead and arrow-key navigation, and a sliding highlight.