All recipes
Default
Default survey
DefaultSurvey is the component the SDK uses for every survey step unless you pass components.Survey. It renders a radio group of reasons, an optional follow-up textarea for reasons with freeform: true, and a continue button gated on selection. The markup uses the SDK's standard ck-step classes, so it inherits whatever you've set on appearance and classNames. Fork it when className overrides can't reach the layout you need — two-column reasons with images, custom card-shaped reasons, anything that needs different markup.
default component
Why are you leaving?
The full source
Copy this file into your project. It's the same file the showcase preview above renders from.
components/MySurvey.tsxtsx
import type { ReasonButtonProps, SurveyStepProps } from '../../core/types'
import { cn } from '../../core/utils'
import { RichText } from '../rich-text'
import { Checkmark } from './shared'
function DefaultReasonButton({ reason, index, isSelected, onSelect }: ReasonButtonProps) {
const letter = String.fromCharCode(65 + index)
return (
<button
type="button"
role="radio"
aria-checked={isSelected}
onClick={() => onSelect(reason.id)}
className={cn('ck-reason-button', isSelected && 'ck-reason-button--selected')}
>
<span aria-hidden className="ck-reason-badge">
{isSelected ? <Checkmark color="#fff" size={12} /> : letter}
</span>
<span className="ck-reason-label">{reason.label}</span>
</button>
)
}
export function DefaultSurvey({
title,
description,
reasons,
selectedReason,
onSelectReason,
followupResponse,
onFollowupResponseChange,
onNext,
classNames,
components,
}: SurveyStepProps) {
const ReasonButton = components?.ReasonButton ?? DefaultReasonButton
const selected = reasons.find((r) => r.id === selectedReason)
const showFollowup = selected?.freeform === true
return (
<div className={cn('ck-step ck-step-survey', classNames?.root)}>
<h2 className={cn('ck-step-title', classNames?.title)}>{title}</h2>
{description && <RichText html={description} className={cn('ck-step-description', classNames?.description)} />}
<div className={cn('ck-reason-list', classNames?.reasonList)} role="radiogroup" aria-label={title}>
{reasons.map((reason, i) => (
<ReasonButton
key={reason.id}
reason={reason}
index={i}
isSelected={selectedReason === reason.id}
onSelect={onSelectReason}
/>
))}
</div>
{showFollowup && (
<textarea
className={cn('ck-reason-followup', classNames?.followupInput)}
placeholder="Tell us more (optional)"
rows={3}
value={followupResponse}
onChange={(e) => onFollowupResponseChange(e.target.value)}
aria-label="Additional detail"
/>
)}
<button
type="button"
className={cn('ck-button ck-button-primary', classNames?.continueButton)}
onClick={onNext}
disabled={!selectedReason}
>
Continue
</button>
</div>
)
}
export { DefaultReasonButton }
Wire it into a flow
Drop the file into your codebase and reference it from the appropriate prop on <CancelFlow>.
CancelButton.tsxtsx
// The SDK uses DefaultSurvey automatically on survey steps.
// Copy the source into your own component to fork it from this baseline.
import { CancelFlow } from '@churnkey/react'
import { MySurvey } from './components/MySurvey' // your fork
<CancelFlow
steps={[
{
type: 'survey',
title: 'Why are you leaving?',
reasons: [
{ id: 'expensive', label: 'Too expensive' },
{ id: 'not-using', label: 'Not using it enough' },
{ id: 'missing', label: 'Missing a feature I need' },
],
},
{ type: 'confirm' },
]}
components={{ Survey: MySurvey }}
handleCancel={async () => myBilling.cancel()}
/>