Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Add To Cart
Add to cart dialog with product image, variant selection, quantity picker, and price display
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Add products to cart with variant selection. This React add to cart dialog provides product image and details display, size and color variant selectors, quantity picker with stock availability, and dynamic price calculation with add to cart button. Built with shadcn/ui Dialog, RadioGroup, Select, Button, and Badge components using Tailwind CSS, users configure products before adding to cart. Select options, adjust quantity, add to cart—perfect for e-commerce stores, product pages, quick-view modals, or any Next.js application with shopping cart functionality requiring variant selection and quantity input.
"use client";import { useState, useMemo } from "react";import { ShoppingCart, Minus, Plus, Check, Loader2, Heart, AlertCircle, Package,} from "lucide-react";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";import { Label } from "@/components/ui/label";import { Badge } from "@/components/ui/badge";import { Separator } from "@/components/ui/separator";import { cn } from "@/lib/utils";type Variant = { id: string; name: string; available: boolean; priceAdjustment?: number;};type Product = { id: string; name: string; description: string; price: number; originalPrice?: number; image: string; sizes: Variant[]; colors: Variant[]; stock: number;};const product: Product = { id: "1", name: "Premium Cotton T-Shirt", description: "Soft, breathable cotton with a modern fit. Perfect for everyday wear.", price: 39.99, originalPrice: 49.99, image: "https://picsum.photos/seed/product1/400/400", sizes: [ { id: "xs", name: "XS", available: true }, { id: "s", name: "S", available: true }, { id: "m", name: "M", available: true }, { id: "l", name: "L", available: true }, { id: "xl", name: "XL", available: false }, ], colors: [ { id: "black", name: "Black", available: true }, { id: "white", name: "White", available: true }, { id: "navy", name: "Navy", available: true, priceAdjustment: 5 }, { id: "gray", name: "Gray", available: false }, ], stock: 12,};export const title = "React Dialog Block Add To Cart";export default function DialogAddToCart() { const [open, setOpen] = useState(false); const [selectedSize, setSelectedSize] = useState<string>(""); const [selectedColor, setSelectedColor] = useState<string>("black"); const [quantity, setQuantity] = useState(1); const [isAdding, setIsAdding] = useState(false); const [isWishlisted, setIsWishlisted] = useState(false); const selectedColorVariant = product.colors.find((c) => c.id === selectedColor); const priceAdjustment = selectedColorVariant?.priceAdjustment || 0; const unitPrice = product.price + priceAdjustment; const totalPrice = unitPrice * quantity; const isLowStock = product.stock <= 5; const canAdd = selectedSize && selectedColor && quantity <= product.stock; const handleIncrement = () => { if (quantity < product.stock) { setQuantity((prev) => prev + 1); } }; const handleDecrement = () => { if (quantity > 1) { setQuantity((prev) => prev - 1); } }; const handleAddToCart = async () => { if (!canAdd) return; setIsAdding(true); await new Promise((resolve) => setTimeout(resolve, 1000)); setIsAdding(false); setOpen(false); // Reset for next time setSelectedSize(""); setQuantity(1); }; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button> <ShoppingCart className="mr-2 h-4 w-4" /> Add to Cart </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-lg"> <DialogHeader> <DialogTitle>Add to Cart</DialogTitle> <DialogDescription> Select your options and quantity. </DialogDescription> </DialogHeader> <div className="space-y-6 py-4"> {/* Product Info */} <div className="flex gap-4"> <div className="h-24 w-24 rounded-lg border overflow-hidden flex-shrink-0"> <img src={product.image} alt={product.name} className="h-full w-full object-cover" /> </div> <div className="flex-1 min-w-0"> <h3 className="font-semibold truncate">{product.name}</h3> <p className="text-sm text-muted-foreground line-clamp-2"> {product.description} </p> <div className="flex items-center gap-2 mt-2"> <span className="font-bold">${unitPrice.toFixed(2)}</span> {product.originalPrice && ( <span className="text-sm text-muted-foreground line-through"> ${product.originalPrice.toFixed(2)} </span> )} {product.originalPrice && ( <Badge variant="destructive" className="text-xs"> Sale </Badge> )} </div> </div> </div> <Separator /> {/* Size Selection */} <div className="space-y-3"> <div className="flex items-center justify-between"> <Label>Size</Label> {!selectedSize && ( <span className="text-xs text-destructive">Required</span> )} </div> <RadioGroup value={selectedSize} onValueChange={setSelectedSize} className="flex flex-wrap gap-2" > {product.sizes.map((size) => ( <div key={size.id}> <RadioGroupItem value={size.id} id={`size-${size.id}`} disabled={!size.available} className="peer sr-only" /> <Label htmlFor={`size-${size.id}`} className={cn( "flex h-10 w-12 cursor-pointer items-center justify-center rounded-md border text-sm font-medium transition-colors", "peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary peer-data-[state=checked]:text-primary-foreground", !size.available && "cursor-not-allowed opacity-50 line-through" )} > {size.name} </Label> </div> ))} </RadioGroup> </div> {/* Color Selection */} <div className="space-y-3"> <Label>Color: {selectedColorVariant?.name}</Label> <RadioGroup value={selectedColor} onValueChange={setSelectedColor} className="flex flex-wrap gap-2" > {product.colors.map((color) => ( <div key={color.id}> <RadioGroupItem value={color.id} id={`color-${color.id}`} disabled={!color.available} className="peer sr-only" /> <Label htmlFor={`color-${color.id}`} className={cn( "flex h-10 px-4 cursor-pointer items-center justify-center rounded-md border text-sm font-medium transition-colors", "peer-data-[state=checked]:border-primary peer-data-[state=checked]:bg-primary peer-data-[state=checked]:text-primary-foreground", !color.available && "cursor-not-allowed opacity-50 line-through" )} > {color.name} {color.priceAdjustment && color.available && ( <span className="ml-1 text-xs">+${color.priceAdjustment}</span> )} </Label> </div> ))} </RadioGroup> </div> {/* Quantity */} <div className="space-y-3"> <div className="flex items-center justify-between"> <Label>Quantity</Label> <div className="flex items-center gap-2"> {isLowStock ? ( <Badge variant="destructive" className="text-xs gap-1"> <AlertCircle className="h-3 w-3" /> Only {product.stock} left </Badge> ) : ( <span className="text-xs text-muted-foreground"> {product.stock} in stock </span> )} </div> </div> <div className="flex items-center gap-3"> <Button variant="outline" size="icon" className="h-10 w-10" onClick={handleDecrement} disabled={quantity <= 1} > <Minus className="h-4 w-4" /> </Button> <span className="w-12 text-center text-lg font-semibold"> {quantity} </span> <Button variant="outline" size="icon" className="h-10 w-10" onClick={handleIncrement} disabled={quantity >= product.stock} > <Plus className="h-4 w-4" /> </Button> </div> </div> <Separator /> {/* Total */} <div className="flex items-center justify-between"> <div> <p className="text-sm text-muted-foreground">Total</p> <p className="text-2xl font-bold">${totalPrice.toFixed(2)}</p> </div> <Button variant="ghost" size="icon" className={cn(isWishlisted && "text-destructive")} onClick={() => setIsWishlisted(!isWishlisted)} > <Heart className={cn("h-5 w-5", isWishlisted && "fill-current")} /> </Button> </div> </div> <DialogFooter className="flex-col sm:flex-row gap-2"> <Button variant="outline" onClick={() => setOpen(false)} disabled={isAdding} className="sm:flex-1" > Continue Shopping </Button> <Button onClick={handleAddToCart} disabled={!canAdd || isAdding} className="sm:flex-1" > {isAdding ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Adding... </> ) : ( <> <ShoppingCart className="mr-2 h-4 w-4" /> Add to Cart </> )} </Button> </DialogFooter> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-add-to-cart.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-add-to-cart.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-add-to-cart.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-add-to-cart.jsonRelated blocks you will also like
React Dialog Block Select Quantity
Quantity selection
React Dialog Block Apply Coupon
Discount codes
React Dialog Block Shipping Options
Shipping selection
React Dialog Block Image Preview
Product images
React Dialog Block Success Confirmation
Added to cart
React Dialog Block Confirm Action
Confirm add
Questions you might have
React Dialog Block Add Writer Profile
Add writer profile dialog with avatar upload, image preview with remove option, author name and title inputs in split layout
React Dialog Block Add To Collection
Add to collection dialog with collection list, create new option, search filter, and multi-select support