Cancellation exit survey in a dialog opened from a plan card, asking a required reason then an optional what would have changed your mind question.
registry/blocks/survey/3/survey-block.tsx
"use client"
import * as React from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoiceDescription,
QuestionnaireChoices,
QuestionnaireDescription,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireNext,
QuestionnairePrevious,
QuestionnaireProgress,
QuestionnaireSkip,
QuestionnaireSubmit,
QuestionnaireTitle,
} from "@/components/ui/questionnaire"
import { Separator } from "@/components/ui/separator"
import { Toaster } from "@/components/ui/sonner"
const REASONS = [
{
value: "price",
label: "Too Expensive",
description: "The plan costs more than the value we get from it.",
},
{
value: "missing",
label: "Missing A Feature",
description: "Something we depend on is not in the product yet.",
},
{
value: "switched",
label: "Switched To Another Tool",
description: "We moved this work somewhere else.",
},
{
value: "unused",
label: "Not Using It Enough",
description: "We never built the habit of opening it.",
},
]
const CHANGED_MIND = [
{ value: "discount", label: "A Lower Price" },
{ value: "onboarding", label: "Hands On Setup Help" },
{ value: "roadmap", label: "A Date For The Missing Feature" },
{ value: "nothing", label: "Nothing, The Decision Is Final" },
]
const ITEMS = [
{ name: "reason", required: true, choices: REASONS },
{ name: "changed-mind", choices: CHANGED_MIND },
]
export default function SurveyBlock() {
const [open, setOpen] = React.useState(true)
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
const answers = new FormData(event.currentTarget)
const reason = String(answers.get("reason") ?? "")
const label =
REASONS.find((item) => item.value === reason)?.label ?? "Your reason"
setOpen(false)
toast.success("Your Plan Is Cancelled", {
description: `We recorded "${label}". Acme stays available until the end of the billing period.`,
})
}
return (
<section className="flex min-h-svh w-full items-center justify-center bg-background px-4 py-12 text-foreground sm:px-6 sm:py-16">
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6">
<p className="text-xs font-medium text-muted-foreground">
Billing And Plan
</p>
<p className="mt-2 text-2xl font-semibold tabular-nums">
$49
<span className="text-sm font-normal text-muted-foreground">
{" "}
per month
</span>
</p>
<p className="mt-1 text-sm text-muted-foreground">
Team plan, renews on 14 September 2026.
</p>
<Separator className="my-5" />
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger
render={<Button variant="outline" className="w-full" />}
>
Cancel Subscription
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<Questionnaire items={ITEMS} onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Before You Go</DialogTitle>
<DialogDescription>
Two questions, and they genuinely change what we build next.
</DialogDescription>
<QuestionnaireProgress />
</DialogHeader>
<div className="min-h-88">
<QuestionnaireItem name="reason" required>
<QuestionnaireTitle>
Why Are You Cancelling?
</QuestionnaireTitle>
<QuestionnaireDescription>
Pick the closest match, or write your own.
</QuestionnaireDescription>
<QuestionnaireChoices>
{REASONS.map((reason) => (
<QuestionnaireChoice
className="rounded-lg"
key={reason.value}
value={reason.value}
>
<span className="font-medium">{reason.label}</span>
<QuestionnaireChoiceDescription>
{reason.description}
</QuestionnaireChoiceDescription>
</QuestionnaireChoice>
))}
<QuestionnaireInput
className="rounded-lg"
aria-label="Another Reason"
placeholder="Another Reason"
/>
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
<QuestionnaireItem name="changed-mind">
<QuestionnaireTitle>
What Would Have Changed Your Mind?
</QuestionnaireTitle>
<QuestionnaireDescription>
Optional. Skip it and we will still process the
cancellation.
</QuestionnaireDescription>
<QuestionnaireChoices>
{CHANGED_MIND.map((option) => (
<QuestionnaireChoice
className="items-center [&_[data-slot=questionnaire-choice-indicator]]:translate-y-0 rounded-lg"
key={option.value}
value={option.value}
>
<span className="font-medium">{option.label}</span>
</QuestionnaireChoice>
))}
</QuestionnaireChoices>
<QuestionnaireError />
</QuestionnaireItem>
</div>
<DialogFooter>
<QuestionnaireActions>
<QuestionnairePrevious />
<QuestionnaireSkip variant="ghost" />
<QuestionnaireNext />
<QuestionnaireSubmit>Cancel Plan</QuestionnaireSubmit>
</QuestionnaireActions>
</DialogFooter>
</Questionnaire>
</DialogContent>
</Dialog>
</div>
<Toaster />
</section>
)
}
Requires a base-* style in components.json (the shadcn init default). Legacy new-york/radix-* styles install Radix primitives, which reject the render prop this block uses.