Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Subscription Cancellation
Subscription cancellation dialog with reason selection, retention offers, feedback collection, and final confirmation
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Reduce churn with thoughtful offboarding. This React subscription cancellation dialog guides users through reason selection, presents retention offers, and collects valuable feedback before finalizing. Built with shadcn/ui Dialog, RadioGroup, Textarea, Button, Alert components and Lucide React icons, the flow gives users pause to reconsider while respecting their decision. Cancellation reasons, discount offers, pause option, feedback collection—perfect for SaaS products, subscription services, or any recurring billing platform where understanding churn improves retention.
"use client";import { useState } from "react";import { AlertTriangle, Gift, Pause, X, ArrowRight, Check, Heart,} from "lucide-react";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { Label } from "@/components/ui/label";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";import { Textarea } from "@/components/ui/textarea";import { Separator } from "@/components/ui/separator";import { Badge } from "@/components/ui/badge";const cancellationReasons = [ { id: "expensive", label: "Too expensive", offer: "discount" }, { id: "not-using", label: "Not using it enough", offer: "pause" }, { id: "missing-features", label: "Missing features I need", offer: "feedback" }, { id: "alternative", label: "Switching to another service", offer: "compare" }, { id: "technical", label: "Technical issues", offer: "support" }, { id: "other", label: "Other reason", offer: "feedback" },];type Step = "reason" | "offer" | "feedback" | "confirm";export const title = "React Dialog Block Subscription Cancellation";export default function DialogCancelSubscription() { const [open, setOpen] = useState(false); const [step, setStep] = useState<Step>("reason"); const [reason, setReason] = useState(""); const [feedback, setFeedback] = useState(""); const [cancelled, setCancelled] = useState(false); const selectedReason = cancellationReasons.find((r) => r.id === reason); const handleNext = () => { if (step === "reason" && reason) { setStep("offer"); } else if (step === "offer") { setStep("feedback"); } else if (step === "feedback") { setStep("confirm"); } }; const handleCancel = () => { console.log({ reason, feedback }); setCancelled(true); }; const handleClose = () => { setOpen(false); setTimeout(() => { setStep("reason"); setReason(""); setFeedback(""); setCancelled(false); }, 200); }; const handleKeepSubscription = () => { handleClose(); }; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <X className="mr-2 h-4 w-4" /> Cancel Subscription </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-md"> {cancelled ? ( <> <DialogHeader className="text-center"> <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-muted mb-4"> <Check className="h-6 w-6" /> </div> <DialogTitle>Subscription Cancelled</DialogTitle> <DialogDescription className="space-y-2"> <p> Your subscription has been cancelled. You'll have access until <strong>January 15, 2025</strong>. </p> <p className="text-xs"> Changed your mind? You can reactivate anytime from your account settings. </p> </DialogDescription> </DialogHeader> <DialogFooter> <Button onClick={handleClose} className="w-full"> Done </Button> </DialogFooter> </> ) : ( <> {step === "reason" && ( <> <DialogHeader> <DialogTitle>We're sorry to see you go</DialogTitle> <DialogDescription> Help us improve by telling us why you're cancelling. </DialogDescription> </DialogHeader> <div className="py-4"> <RadioGroup value={reason} onValueChange={setReason}> <div className="space-y-2"> {cancellationReasons.map((option) => ( <div key={option.id} className="flex items-center space-x-3 rounded-lg border p-3 hover:bg-muted cursor-pointer" onClick={() => setReason(option.id)} > <RadioGroupItem value={option.id} id={option.id} /> <Label htmlFor={option.id} className="flex-1 cursor-pointer" > {option.label} </Label> </div> ))} </div> </RadioGroup> </div> <DialogFooter className="gap-2 sm:gap-0"> <Button variant="outline" onClick={handleClose}> Never mind </Button> <Button onClick={handleNext} disabled={!reason}> Continue <ArrowRight className="ml-2 h-4 w-4" /> </Button> </DialogFooter> </> )} {step === "offer" && ( <> <DialogHeader> <DialogTitle>Before you go...</DialogTitle> <DialogDescription> We'd love to keep you! Here's a special offer. </DialogDescription> </DialogHeader> <div className="py-4 space-y-4"> {selectedReason?.offer === "discount" && ( <div className="rounded-lg border-2 border-primary bg-primary/5 p-4 space-y-3"> <div className="flex items-center gap-2"> <Gift className="h-5 w-5 text-primary" /> <span className="font-semibold">Special Offer</span> <Badge className="ml-auto">50% OFF</Badge> </div> <p className="text-sm text-muted-foreground"> Stay with us and get 50% off your next 3 months. That's just $6/month instead of $12. </p> <Button className="w-full" onClick={handleKeepSubscription}> <Heart className="mr-2 h-4 w-4" /> Accept Offer & Stay </Button> </div> )} {selectedReason?.offer === "pause" && ( <div className="rounded-lg border-2 border-primary bg-primary/5 p-4 space-y-3"> <div className="flex items-center gap-2"> <Pause className="h-5 w-5 text-primary" /> <span className="font-semibold">Take a Break</span> </div> <p className="text-sm text-muted-foreground"> Not using it enough? Pause your subscription for up to 3 months instead of cancelling. Your data stays safe. </p> <Button className="w-full" onClick={handleKeepSubscription}> <Pause className="mr-2 h-4 w-4" /> Pause for 1 Month </Button> </div> )} {(selectedReason?.offer === "feedback" || selectedReason?.offer === "support" || selectedReason?.offer === "compare") && ( <div className="rounded-lg border bg-muted/50 p-4 space-y-3"> <div className="flex items-center gap-2"> <AlertTriangle className="h-5 w-5 text-amber-500" /> <span className="font-semibold">We want to help</span> </div> <p className="text-sm text-muted-foreground"> {selectedReason?.offer === "support" ? "Our support team can help resolve technical issues. Would you like to chat with us first?" : selectedReason?.offer === "compare" ? "We'd love to understand what's working better elsewhere. Your feedback helps us improve." : "We're constantly improving. Tell us what features you need and we'll prioritize them."} </p> <Button variant="outline" className="w-full" onClick={handleKeepSubscription} > Talk to Us First </Button> </div> )} <Separator /> <Button variant="ghost" className="w-full text-muted-foreground" onClick={handleNext} > No thanks, continue cancelling </Button> </div> </> )} {step === "feedback" && ( <> <DialogHeader> <DialogTitle>One last thing</DialogTitle> <DialogDescription> Your feedback helps us build a better product. </DialogDescription> </DialogHeader> <div className="py-4 space-y-4"> <div className="space-y-2"> <Label htmlFor="feedback"> What could we have done better? (Optional) </Label> <Textarea id="feedback" placeholder="Tell us what would have made you stay..." value={feedback} onChange={(e) => setFeedback(e.target.value)} className="min-h-[100px] resize-none" /> </div> </div> <DialogFooter className="gap-2 sm:gap-0"> <Button variant="outline" onClick={() => setStep("offer")}> Back </Button> <Button onClick={handleNext}> Continue <ArrowRight className="ml-2 h-4 w-4" /> </Button> </DialogFooter> </> )} {step === "confirm" && ( <> <DialogHeader> <div className="flex items-center gap-3"> <div className="flex h-10 w-10 items-center justify-center rounded-full bg-destructive/10"> <AlertTriangle className="h-5 w-5 text-destructive" /> </div> <div> <DialogTitle>Confirm Cancellation</DialogTitle> <DialogDescription> This action cannot be undone. </DialogDescription> </div> </div> </DialogHeader> <div className="py-4"> <div className="rounded-lg border bg-muted/50 p-4 space-y-2 text-sm"> <p> <strong>What happens next:</strong> </p> <ul className="list-disc list-inside space-y-1 text-muted-foreground"> <li>Your access continues until January 15, 2025</li> <li>You won't be charged again</li> <li>Your data will be retained for 30 days</li> <li>You can reactivate anytime</li> </ul> </div> </div> <DialogFooter className="gap-2 sm:gap-0"> <Button variant="outline" onClick={handleKeepSubscription}> Keep Subscription </Button> <Button variant="destructive" onClick={handleCancel}> Cancel Subscription </Button> </DialogFooter> </> )} </> )} </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-cancel-subscription.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-cancel-subscription.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-cancel-subscription.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-cancel-subscription.jsonRelated blocks you will also like
React Dialog Block Upgrade Plan
Pricing tier comparison
React Dialog Block Destructive Warning
Warning before destructive action
React Dialog Block Password Confirm
Confirmation with password
React Dialog Block Feedback Rating
Feedback with rating scale
React Dialog Block Report Content
Reason selection form
React Dialog Block Two Actions
Dialog with two options