Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Split Payment
Split payment dialog with multiple payment methods, amount allocation, and balance calculation
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Pay with multiple methods seamlessly. This React split payment dialog provides multiple payment method selection, custom amount allocation per method, running balance display with remaining amount, and validation to ensure full payment. Built with shadcn/ui Dialog, Input, Button, DropdownMenu, and Separator components using Tailwind CSS, users split transactions flexibly. Add methods from dropdown, set amounts, verify total—perfect for e-commerce checkout, restaurant POS, group payments, or any Next.js application requiring split tender functionality with real-time balance tracking.
"use client";import { useState, useMemo } from "react";import { Split, CreditCard, Gift, Wallet, X, Loader2, Check, DollarSign, Plus } from "lucide-react";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { Input } from "@/components/ui/input";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu";import { Separator } from "@/components/ui/separator";import { cn } from "@/lib/utils";type PaymentMethodType = "card" | "gift_card" | "wallet" | "store_credit";type PaymentMethod = { id: string; type: PaymentMethodType; label: string; detail: string; icon: React.ElementType; maxAmount?: number;};type PaymentSplit = { methodId: string; amount: number;};const availableMethods: PaymentMethod[] = [ { id: "card1", type: "card", label: "Visa", detail: "••••4242", icon: CreditCard }, { id: "card2", type: "card", label: "Mastercard", detail: "••••8888", icon: CreditCard }, { id: "gift1", type: "gift_card", label: "Gift Card", detail: "Balance: $25.00", icon: Gift, maxAmount: 25 }, { id: "wallet", type: "wallet", label: "Apple Pay", detail: "Connected", icon: Wallet }, { id: "credit", type: "store_credit", label: "Store Credit", detail: "Balance: $15.50", icon: DollarSign, maxAmount: 15.5 },];const orderTotal = 89.99;export const title = "React Dialog Block Split Payment";export default function DialogSplitPayment() { const [open, setOpen] = useState(false); const [splits, setSplits] = useState<PaymentSplit[]>([ { methodId: "card1", amount: orderTotal }, ]); const [isProcessing, setIsProcessing] = useState(false); const totalAllocated = useMemo( () => splits.reduce((sum, s) => sum + s.amount, 0), [splits] ); const remaining = useMemo( () => Math.max(0, orderTotal - totalAllocated), [totalAllocated] ); const isOverpaid = totalAllocated > orderTotal; const isFullyPaid = Math.abs(totalAllocated - orderTotal) < 0.01; const usedMethodIds = splits.map((s) => s.methodId); const availableToAdd = availableMethods.filter( (m) => !usedMethodIds.includes(m.id) ); const handleAddMethod = (methodId: string) => { const method = availableMethods.find((m) => m.id === methodId); if (!method) return; const amount = Math.min(remaining, method.maxAmount || remaining); setSplits([...splits, { methodId, amount }]); }; const handleRemoveMethod = (methodId: string) => { if (splits.length <= 1) return; setSplits(splits.filter((s) => s.methodId !== methodId)); }; const handleAmountChange = (methodId: string, value: string) => { const amount = parseFloat(value) || 0; const method = availableMethods.find((m) => m.id === methodId); const maxAmount = method?.maxAmount || orderTotal; setSplits( splits.map((s) => s.methodId === methodId ? { ...s, amount: Math.min(Math.max(0, amount), maxAmount) } : s ) ); }; const handlePayRemaining = (methodId: string) => { const method = availableMethods.find((m) => m.id === methodId); const currentSplit = splits.find((s) => s.methodId === methodId); if (!currentSplit) return; const maxAmount = method?.maxAmount || Infinity; const newAmount = Math.min(currentSplit.amount + remaining, maxAmount); setSplits( splits.map((s) => s.methodId === methodId ? { ...s, amount: newAmount } : s ) ); }; const handleProcess = async () => { if (!isFullyPaid) return; setIsProcessing(true); await new Promise((resolve) => setTimeout(resolve, 1500)); setIsProcessing(false); setOpen(false); setSplits([{ methodId: "card1", amount: orderTotal }]); }; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Split className="mr-2 h-4 w-4" /> Split Payment </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-sm"> <DialogHeader> <DialogTitle>Split Payment</DialogTitle> <DialogDescription> Split ${orderTotal.toFixed(2)} across multiple methods. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> {/* Payment Methods */} <div className="space-y-3"> {splits.map((split) => { const method = availableMethods.find((m) => m.id === split.methodId); if (!method) return null; const Icon = method.icon; return ( <div key={split.methodId} className="flex items-center gap-3 rounded-lg border p-3" > <Icon className="h-4 w-4 text-muted-foreground" /> <div className="flex-1 min-w-0"> <p className="text-sm font-medium">{method.label}</p> <p className="text-xs text-muted-foreground">{method.detail}</p> </div> <div className="flex items-center gap-1"> <div className="relative"> <span className="absolute left-2 top-1/2 -translate-y-1/2 text-sm text-muted-foreground"> $ </span> <Input type="number" step="0.01" min="0" max={method.maxAmount || orderTotal} value={split.amount.toFixed(2)} onChange={(e) => handleAmountChange(split.methodId, e.target.value)} className="w-24 pl-6 text-right" /> </div> {remaining > 0 && split.amount < (method.maxAmount || Infinity) && ( <Button variant="ghost" size="sm" className="text-xs px-2" onClick={() => handlePayRemaining(split.methodId)} > +${Math.min(remaining, (method.maxAmount || Infinity) - split.amount).toFixed(2)} </Button> )} {splits.length > 1 && ( <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => handleRemoveMethod(split.methodId)} > <X className="h-4 w-4" /> </Button> )} </div> </div> ); })} </div> {/* Add Payment Method */} {availableToAdd.length > 0 && ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" size="sm" className="w-full"> <Plus className="mr-2 h-4 w-4" /> Add Payment Method </Button> </DropdownMenuTrigger> <DropdownMenuContent align="start" className="w-[var(--radix-dropdown-menu-trigger-width)]"> {availableToAdd.map((method) => { const Icon = method.icon; return ( <DropdownMenuItem key={method.id} onClick={() => handleAddMethod(method.id)} > <Icon className="mr-2 h-4 w-4" /> <span className="flex-1">{method.label}</span> <span className="text-xs text-muted-foreground">{method.detail}</span> </DropdownMenuItem> ); })} </DropdownMenuContent> </DropdownMenu> )} <Separator /> {/* Balance Summary */} <div className="space-y-1.5 text-sm"> <div className="flex justify-between"> <span className="text-muted-foreground">Order Total</span> <span>${orderTotal.toFixed(2)}</span> </div> <div className="flex justify-between"> <span className="text-muted-foreground">Allocated</span> <span className={cn(isOverpaid && "text-destructive")}> ${totalAllocated.toFixed(2)} </span> </div> <div className="flex justify-between pt-1.5 border-t font-medium"> <span>Remaining</span> <span className={cn( isFullyPaid && "text-primary", isOverpaid && "text-destructive" )}> {isFullyPaid ? ( <span className="flex items-center gap-1"> <Check className="h-3 w-3" /> Paid </span> ) : isOverpaid ? ( `+$${(totalAllocated - orderTotal).toFixed(2)}` ) : ( `$${remaining.toFixed(2)}` )} </span> </div> </div> {isOverpaid && ( <p className="text-xs text-destructive"> Total exceeds order amount. Please adjust. </p> )} </div> <DialogFooter> <Button variant="outline" onClick={() => setOpen(false)} disabled={isProcessing}> Cancel </Button> <Button onClick={handleProcess} disabled={!isFullyPaid || isProcessing}> {isProcessing ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Processing... </> ) : ( `Pay $${orderTotal.toFixed(2)}` )} </Button> </DialogFooter> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-split-payment.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-split-payment.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-split-payment.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-split-payment.jsonRelated blocks you will also like
React Dialog Block Payment Method
Payment selection
React Dialog Block Add Card
Card entry
React Dialog Block Apply Coupon
Discounts
React Dialog Block Gift Card
Gift card payment
React Dialog Block Billing Address
Billing info
React Dialog Block Success Confirmation
Payment complete