All recipes
Default

Default feedback

DefaultFeedback is the component the SDK uses for every feedback step unless you pass components.Feedback. It wires a textarea to the SDK's feedback state, handles required/minLength validation, and shows a character count when minLength is set. Fork it to attach files, swap the textarea for a structured form, or add AI-suggested follow-ups.

default component

Anything else we should know?

Honest feedback helps us improve. We read every reply.

The full source

Copy this file into your project. It's the same file the showcase preview above renders from.

components/MyFeedback.tsxtsx
import { useState } from 'react'
import type { FeedbackStepProps } from '../../core/types'
import { cn } from '../../core/utils'
import { RichText } from '../rich-text'

export function DefaultFeedback({
  title,
  description,
  placeholder,
  required,
  minLength,
  value,
  onChange,
  onSubmit,
  classNames,
}: FeedbackStepProps) {
  const [focused, setFocused] = useState(false)
  const hasMin = minLength > 0
  const isUnderMin = hasMin && value.length > 0 && value.length < minLength
  const isValid = !required || value.length >= minLength
  const placeholderText = placeholder ?? (hasMin ? `At least ${minLength} characters…` : 'Type your thoughts…')

  return (
    <div className={cn('ck-step ck-step-feedback', 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-feedback-field',
          focused && 'ck-feedback-field--focused',
          isUnderMin && 'ck-feedback-field--invalid',
        )}
      >
        <textarea
          className={cn('ck-textarea', classNames?.textarea)}
          placeholder={placeholderText}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          onFocus={() => setFocused(true)}
          onBlur={() => setFocused(false)}
          rows={3}
        />
        {hasMin && (
          <div
            className={cn(
              'ck-character-count',
              isUnderMin && 'ck-character-count--invalid',
              classNames?.characterCount,
            )}
          >
            {value.length} / {minLength}
          </div>
        )}
      </div>

      <button
        type="button"
        className={cn('ck-button ck-button-primary', classNames?.submitButton)}
        onClick={onSubmit}
        disabled={!isValid}
      >
        Continue
      </button>
    </div>
  )
}

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 DefaultFeedback automatically on feedback steps.
// Copy the source into your own component to fork it from this baseline.

import { CancelFlow } from '@churnkey/react'
import { MyFeedback } from './components/MyFeedback'

<CancelFlow
  steps={[
    { type: 'survey', reasons: [/* ... */] },
    {
      type: 'feedback',
      title: 'Anything else we should know?',
      description: 'Honest feedback helps us improve.',
      placeholder: 'Tell us what we could have done better…',
      required: false,
    },
    { type: 'confirm' },
  ]}
  components={{ Feedback: MyFeedback }}
  handleCancel={async () => myBilling.cancel()}
/>