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