Inputs

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.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<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.json

Or, 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 = 0

Props

PropTypeDefaultDescription
valuenumber | null—Controlled selected score (0-10); null means nothing picked yet, and omitting the prop makes the component uncontrolled
defaultValuenumber | nullnullInitial 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
questionstring"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?")
followUpQuestionstring—Follow-up template; {score} in the template is replaced with the actual score, and it takes precedence over followUps and the built-in defaults
followUpsPartial<Record<NpsCategory, string>>—Per-band follow-up overrides (detractor/passive/promoter), also supporting the {score} placeholder
showLabelsbooleantrueShow the two end labels ("not at all likely" / "extremely likely")
requireCommentbooleanfalseMake the comment mandatory: the submit button stays disabled while it is empty
compactbooleanfalseCompact mode: smaller buttons, type size and spacing
labelsPartial<NpsSurveyCardLabels>—Override the interface copy
classNamestring—Forwarded to the outermost card

NpsSurveyCardLabels

FieldTypeDefaultDescription
lowstring"Absolutely Impossible"Label at the 0 end (default is Chinese for "not at all likely")
highstring"Very Likely"Label at the 10 end (default is Chinese for "extremely likely")
detractorstring"Detractor"Name of the 0-6 band (used in accessibility announcements; Chinese for "detractor")
passivestring"Passive"Name of the 7-8 band (Chinese for "passive")
promoterstring"Recommender"Name of the 9-10 band (Chinese for "promoter")
commentPlaceholderstring"Write Anything You Think Of"Placeholder of the follow-up textarea (Chinese for "write whatever comes to mind")
commentRequiredstring"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")
submitstring"Send"Submit button text (Chinese for "submit")
thanksTitlestring"Thank you for your feedback!"Thank-you title (Chinese for "thanks for your feedback!")
thanksBodystring"We will carefully read every comment."Thank-you body copy (Chinese for "we read every piece of feedback")
resetstring"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-band followUps override, 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 onReset is provided; in controlled mode, set value back to null inside your onReset callback

Accessibility

  • The 0-10 buttons form a role="radiogroup" (named by the main question via aria-labelledby); each score is a role="radio" with aria-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; with requireComment on, aria-required is set and aria-describedby points 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