@tour-kit/surveys
In-app microsurveys: NPS, CSAT, CES, and custom question flows with fatigue prevention, skip logic, and five display modes for React apps
LLM Context File
Working with an AI assistant? Download the context file for @tour-kit/surveys and paste it into your conversation for accurate code generation.
Why userTourKit for in-app surveys →
Pro package
@tour-kit/surveys is a Pro-tier package. A valid license key is required. The SurveysProvider
is wrapped in ProGate — without a license the tree renders nothing and a warning is logged to
the console.
In-app microsurveys that surface at the right moment without overwhelming your users. Three industry-standard survey types (NPS, CSAT, CES) plus fully custom question flows, all delivered through five display modes and protected by a multi-layer fatigue prevention system.
Why surveys?
Point-in-time feedback forms tell you what users think right now, in context — before they forget. A support ticket or app store review is too late and too filtered. Surveys close the loop between behavior data and qualitative intent.
- NPS — "How likely are you to recommend us?" Tracks loyalty trends over time.
- CSAT — "How satisfied were you with this experience?" Pinpoints interaction quality.
- CES — "How easy was it to accomplish your goal?" Surfaces friction before it becomes churn.
- Custom — Multi-step question flows for anything else: feature sentiment, cancellation reasons, onboarding barriers.
Installation
pnpm add @tour-kit/surveysnpm install @tour-kit/surveysyarn add @tour-kit/surveysQuick Start
A minimal NPS survey delivered in a modal. Register the config, mount the display component, and call show() when the moment is right.
import { SurveysProvider, SurveyModal, useSurvey } from '@tour-kit/surveys';
const npsConfig = {
id: 'nps-q1',
type: 'nps',
displayMode: 'modal',
title: 'How likely are you to recommend us?',
questions: [
{
id: 'score',
type: 'rating',
text: 'On a scale of 0 to 10, how likely are you to recommend us to a friend or colleague?',
ratingScale: { min: 0, max: 10, labels: { min: 'Not at all likely', max: 'Extremely likely' } },
},
],
onComplete: (responses) => {
console.log('NPS score:', responses.get('score'));
},
};
function NPSTrigger() {
const survey = useSurvey('nps-q1');
return (
<>
<SurveyModal surveyId="nps-q1" />
<button onClick={survey.show}>Give feedback</button>
</>
);
}
export function App() {
return (
<SurveysProvider surveys={[npsConfig]}>
<NPSTrigger />
</SurveysProvider>
);
}Survey types
| Type | Scale | Scoring |
|---|---|---|
nps | 0–10 rating | Promoters (9–10) minus detractors (0–6), −100 to 100 |
csat | Rating with threshold | % of responses at or above threshold, 0–100 |
ces | 1–7 rating | Average effort score; easy ≥ 5, difficult ≤ 3 |
custom | Any question mix | No automatic scoring |
Display modes
| Mode | Description |
|---|---|
modal | Centered dialog with backdrop |
slideout | Side panel that slides in from left or right |
banner | Top or bottom strip, optionally sticky |
popover | Floating panel anchored to a DOM element |
inline | Embedded directly in the page flow |
Fatigue prevention
Every show() call passes through a gate stack in order. The first failing gate stops evaluation — individual frequency rules never run if the global cooldown blocks.
| Mechanism | Scope | Default |
|---|---|---|
| Global cooldown | Between any two surveys, provider-wide | 14 days |
| Sampling rate | Probability a user sees the survey (0–1) | 1.0 (everyone) |
| Max per session | Hard cap on surveys shown in one session | 1 |
| Frequency rule | Per-survey show count or interval | none |
| Snooze | User-initiated delay with max count | unlimited |
The sampling roll happens once when SurveysProvider mounts, not on each show() call. If a
user's roll fails, no survey from this provider instance shows until remount. To re-roll, remount
the provider.
Frequency rules
frequency?: FrequencyRule
// 'once' — shows exactly once per user
// 'session' — once per session
// 'always' — every time show() is called (ignores cooldown)
// { type: 'times', count: 3 } — show up to N times total
// { type: 'interval', days: 30 } — show at most once every N daysSkip logic
Questions can branch based on previous answers. Define skipLogic on any question:
{
id: 'reason',
type: 'single-select',
text: 'What is your main reason for leaving?',
options: [
{ value: 'price', label: 'Price' },
{ value: 'features', label: 'Missing features' },
{ value: 'other', label: 'Other' },
],
skipLogic: [
{
questionId: 'reason',
condition: (answer) => answer !== 'other',
skipTo: 'confirm', // jump over the free-text follow-up
},
],
}Skip logic cycles are detected by the FlowEngine's visited-step tracker. A cycle that would create an infinite loop is broken at the point of re-entry, but the question order will appear incorrect. Design skip chains as directed acyclic graphs.
Architecture
SurveysProvider
├── Context (useReducer)
│ ├── surveys Map<id, SurveyState>
│ ├── activeSurvey string | null
│ ├── queue string[]
│ └── Gate stack (sampling → cooldown → session → snooze → frequency)
├── Display components
│ ├── SurveyModal, SurveySlideout, SurveyBanner, SurveyPopover, SurveyInline
│ └── Each reads isVisible from SurveyState via useSurvey()
├── Question components
│ ├── QuestionRating, QuestionText, QuestionSelect, QuestionBoolean
│ └── SurveyProgress
├── Headless variants
│ └── HeadlessSurvey, HeadlessQuestion*
└── Scoring utilities
└── calculateNPS, calculateCSAT, calculateCESState management
State is managed via useReducer inside SurveysProvider. Each survey has an independent SurveyState record. The provider serializes state to localStorage (or your adapter) on every change and hydrates on mount.
Partial responses persist immediately. Every answer() call writes to storage, not just on
complete(). If a user closes mid-survey, their partial answers will be in storage on next visit.
Design your onComplete callback to treat the responses Map as canonical, not assume it is
empty.
Bundle size
| Package | Gzipped |
|---|---|
| @tour-kit/surveys | < 12KB |
Package contents
Providers
SurveysProvider — configure fatigue rules, storage, and analytics callbacks
Hooks
useSurvey, useSurveys, useSurveyScoring — programmatic control and state access
Components
Modal, Slideout, Banner, Popover, Inline, and question components
Headless
Build entirely custom survey UI with render props
Utilities
calculateNPS, calculateCSAT, calculateCES — scoring functions
Types
TypeScript type definitions and interfaces
Accessibility
All display and question components ship with full ARIA support:
SurveyModalandSurveySlideoutuse@radix-ui/react-dialog— focus is trapped inside the open dialog and restored on closeSurveyBannerandSurveyInlineuserole="region"with descriptivearia-labelQuestionRatingandQuestionBooleanimplement roving tabindex on arole="radiogroup"containerQuestionSelectusesrole="radiogroup"(single) orrole="group"(multi) with per-optionrole="radio"/role="checkbox"SurveyProgressusesrole="progressbar"witharia-valuenow,aria-valuemin,aria-valuemax- Character counts in
QuestionTextare announced viaaria-live="polite"
Related
- @tour-kit/core — storage adapters used by the provider
- @tour-kit/scheduling — optional peer for time-based survey gating
Ship onboarding, not config.
npm i @tour-kit/core is MIT and free. The Pro packages work unlicensed too — a one-time $99 license removes the production watermark when you ship.
MIT-licensed — no signup, no credit card. Pay once, only when you ship.