# Cancellation Exit Survey Dialog

> 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.

A shadcn/ui block from the [@7ovr](https://7ovr.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/7ovr/survey-3).

- Page, with a live preview: https://shadcn.exchange/r/7ovr/survey-3
- Screenshot: https://shadcn.exchange/shot/7ovr/survey-3/light.png
- Registry item: https://7ovr.com/r/survey-3.json
- Categories: survey
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/7ovr/survey-3.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`
- Registry items: `button`, `dialog`, `questionnaire`, `separator`, `sonner`

## Notes from the registry

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.

## Files

### `registry/blocks/survey/3/survey-block.tsx`

```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>
  )
}
```
