Checkout Modal
A multi-step checkout flow with plan review, billing details form, payment method input, and confirmation screen.
Preview
Features
- 4-step checkout flow: Review → Details → Payment → Confirm
- Visual step indicator with filled backgrounds and ring styling
- Grouped form sections with icons (User, MapPin, CreditCard)
- Mobile-friendly layout with wrapped steps, stacked actions, and scrollable dialog
- Security badge with emerald color scheme
- Success confirmation with order summary
- Accessible with keyboard navigation
Install Summary
This kit will add the following files and dependencies to your project. Download the bundle and extract it into your project root.
Component Files
| File | Path |
|---|---|
| checkout-modal.tsx | components/billing/checkout-modal/checkout-modal.tsx |
| checkout-context.tsx | components/billing/checkout-modal/checkout-context.tsx |
| checkout-steps.tsx | components/billing/checkout-modal/checkout-steps.tsx |
| step-review.tsx | components/billing/checkout-modal/step-review.tsx |
| step-details.tsx | components/billing/checkout-modal/step-details.tsx |
| step-payment.tsx | components/billing/checkout-modal/step-payment.tsx |
| step-confirm.tsx | components/billing/checkout-modal/step-confirm.tsx |
| types.ts | components/billing/checkout-modal/types.ts |
| index.ts | components/billing/checkout-modal/index.ts |
Utility Files
| File | Path |
|---|---|
| format.ts | lib/format.ts |
| design-tokens.ts | lib/design-tokens.ts |
Dependencies
Install these dependencies before using the component:
Terminal
bash
npx shadcn@latest add dialog button input labelTerminal
bash
npm install @phosphor-icons/reactInstallation
Download the complete bundle as a ZIP file, or copy the text bundle to your clipboard:
Component Files
The component consists of the following files:
1. checkout-modal.tsx
checkout-modal.tsx
tsx
"use client";
import {
Dialog,
DialogContent,2. checkout-context.tsx
checkout-context.tsx
tsx
"use client";
import {
createContext,
useContext,3. checkout-steps.tsx
checkout-steps.tsx
tsx
"use client";
import { Check } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
import type { CheckoutStep } from "./types";4. step-review.tsx
step-review.tsx
tsx
"use client";
import { Receipt } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
import { formatPrice } from "@/lib/format";5. step-details.tsx
step-details.tsx
tsx
"use client";
import { useState } from "react";
import { User, MapPin } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";6. step-payment.tsx
step-payment.tsx
tsx
"use client";
import { useState } from "react";
import { CreditCard, Lock, ShieldCheck } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";7. step-confirm.tsx
step-confirm.tsx
tsx
"use client";
import { SealCheck as CheckCircle2 } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
import { useCheckout } from "./checkout-context";8. types.ts
types.ts
tsx
export type CheckoutStep = "review" | "details" | "payment" | "confirm";
export interface CheckoutPlan {
id: string;
name: string;9. index.ts
index.ts
tsx
export { CheckoutModal } from "./checkout-modal";
export type {
CheckoutStep,
CheckoutPlan,
BillingAddress,
BillingDetails,
CheckoutModalProps,
} from "./types";
Shared Utilities
This component uses shared utility functions. These are included in the bundle above:
format.ts
format.ts
tsx
export function formatDate(date: Date): string {
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
}
export function formatRelativeTime(date: Date, now: Date = new Date()): string {
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return "Just now";
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days < 7) return `${days}d ago`;
return formatDate(date);
}
export function formatPrice(amount: number, currency = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
minimumFractionDigits: 0,
maximumFractionDigits: 2,
}).format(amount);
}
export function formatNumber(num: number): string {
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
return num.toString();
}
design-tokens.ts
design-tokens.ts
tsx
// Border radius
export const radius = {
card: "rounded-xl",
badge: "rounded-full",
button: "rounded-lg",Usage
app/billing/page.tsx
tsx
"use client";
import { useState } from "react";
import { CheckoutModal, type CheckoutPlan } from "@/components/billing";
import { Button } from "@/components/ui/button";Props
| Prop | Type | Default |
|---|---|---|
| open | boolean | Required |
| onOpenChange | (open: boolean) => void | Required |
| plan | CheckoutPlan | Required |
| onComplete | (details: BillingDetails) => void | - |
| onCancel | () => void | - |
| className | string | - |