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()}
/>