ShadCN Vaults

Back to Blocks

Animated Subscription Billing

Dashboard Bills Block

Dashboard BillsComponent

Animated Subscription Billing

Modern dashboard with animated add-on selection, plan management, and detailed billing.

Preview

Full width desktop view

Code

bills-9.tsx
1"use client";
2
3import * as React from "react";
4import { useState, useMemo } from "react";
5import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
6import { Button } from "@/components/ui/button";
7import { Badge } from "@/components/ui/badge";
8import { Separator } from "@/components/ui/separator";
9import { Switch } from "@/components/ui/switch";
10import {
11  Check,
12  CreditCard,
13  Calendar,
14  DollarSign,
15  Users,
16  TrendingUp,
17  AlertCircle,
18  Plus,
19  X,
20} from "lucide-react";
21import { AnimatePresence, motion } from "framer-motion";
22import { cn } from "@/lib/utils";
23
24// Types
25interface Plan {
26  id: string;
27  name: string;
28  description: string;
29  monthlyPrice: number;
30  annualPrice: number;
31  features: string[];
32  popular?: boolean;
33  maxUsers?: number;
34  storage?: string;
35}
36
37interface AddOn {
38  id: string;
39  name: string;
40  description: string;
41  monthlyPrice: number;
42  annualPrice: number;
43  category: string;
44}
45
46interface BillItem {
47  id: string;
48  name: string;
49  type: "plan" | "addon";
50  quantity: number;
51  unitPrice: number;
52  total: number;
53}
54
55interface CustomerData {
56  companyName: string;
57  currentPlan?: string;
58  billingCycle: "monthly" | "annual";
59  nextBillingDate: string;
60  totalUsers: number;
61}
62
63// Tag Component for Add-ons
64const Tag = ({
65  children,
66  className,
67  name,
68  onClick,
69}: {
70  children: React.ReactNode;
71  className?: string;
72  name?: string;
73  onClick?: () => void;
74}) => {
75  return (
76    <motion.div
77      layout
78      layoutId={name}
79      onClick={onClick}
80      className={cn(
81        "cursor-pointer rounded-md bg-gray-200 px-2 py-1 text-sm",
82        className
83      )}
84    >
85      {children}
86    </motion.div>
87  );
88};
89
90// Multiple Select Component for Add-ons
91const MultipleSelect = ({
92  tags,
93  onChange,
94  defaultValue = [],
95}: {
96  tags: AddOn[];
97  onChange?: (value: AddOn[]) => void;
98  defaultValue?: AddOn[];
99}) => {
100  const [selected, setSelected] = useState<AddOn[]>(defaultValue);
101  const containerRef = React.useRef<HTMLDivElement>(null);
102
103  React.useEffect(() => {
104    if (containerRef?.current) {
105      containerRef.current.scrollBy({
106        left: containerRef.current?.scrollWidth,
107        behavior: "smooth",
108      });
109    }
110    onChange?.(selected);
111  }, [selected, onChange]);
112
113  const onSelect = (item: AddOn) => {
114    setSelected((prev) => [...prev, item]);
115  };
116
117  const onDeselect = (item: AddOn) => {
118    setSelected((prev) => prev.filter((i) => i.id !== item.id));
119  };
120
121  return (
122    <AnimatePresence mode={"popLayout"}>
123      <div className="flex w-full flex-col gap-3">
124        <div className="flex items-center justify-between">
125          <h3 className="text-lg font-semibold text-foreground">Add-ons</h3>
126          <Badge variant="outline">{selected.length} selected</Badge>
127        </div>
128
129        <motion.div
130          layout
131          ref={containerRef}
132          className="selected no-scrollbar flex min-h-12 w-full items-center overflow-x-scroll scroll-smooth rounded-md border border-border bg-muted/30 p-2"
133        >
134          <motion.div layout className="flex items-center gap-2">
135            {selected?.map((item) => (
136              <Tag
137                name={item?.id}
138                key={item?.id}
139                className="bg-background shadow-sm border"
140              >
141                <div className="flex items-center gap-2">
142                  <motion.span layout className="text-nowrap text-xs">
143                    {item?.name}
144                  </motion.span>
145                  <button onClick={() => onDeselect(item)}>
146                    <X size={12} />
147                  </button>
148                </div>
149              </Tag>
150            ))}
151            {selected.length === 0 && (
152              <span className="text-muted-foreground text-sm">
153                No add-ons selected
154              </span>
155            )}
156          </motion.div>
157        </motion.div>
158
159        {tags?.length > selected?.length && (
160          <div className="flex w-full flex-wrap gap-2 rounded-md border border-border p-3 bg-background">
161            {tags
162              ?.filter((item) => !selected?.some((i) => i.id === item.id))
163              .map((item) => (
164                <Tag
165                  name={item?.id}
166                  onClick={() => onSelect(item)}
167                  key={item?.id}
168                  className="hover:bg-muted transition-colors"
169                >
170                  <motion.div layout className="flex items-center gap-2">
171                    <span className="text-nowrap text-xs">{item?.name}</span>
172                    <span className="text-xs text-muted-foreground">
173                      ${item.monthlyPrice}/mo
174                    </span>
175                    <Plus size={12} />
176                  </motion.div>
177                </Tag>
178              ))}
179          </div>
180        )}
181      </div>
182    </AnimatePresence>
183  );
184};
185
186// Main Dashboard Component
187const CustomerAdminDashboard = () => {
188  const [selectedPlan, setSelectedPlan] = useState<string>("starter");
189  const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">(
190    "monthly"
191  );
192  const [selectedAddOns, setSelectedAddOns] = useState<AddOn[]>([]);
193
194  // Sample data
195  const customerData: CustomerData = {
196    companyName: "Acme Corporation",
197    currentPlan: "starter",
198    billingCycle: "monthly",
199    nextBillingDate: "2024-02-15",
200    totalUsers: 12,
201  };
202
203  const plans: Plan[] = [
204    {
205      id: "free",
206      name: "Free",
207      description: "Perfect for getting started",
208      monthlyPrice: 0,
209      annualPrice: 0,
210      maxUsers: 3,
211      storage: "1GB",
212      features: [
213        "Basic dashboard",
214        "Up to 3 users",
215        "1GB storage",
216        "Email support",
217      ],
218    },
219    {
220      id: "starter",
221      name: "Starter",
222      description: "Great for small teams",
223      monthlyPrice: 29,
224      annualPrice: 290,
225      maxUsers: 10,
226      storage: "10GB",
227      popular: true,
228      features: [
229        "Advanced dashboard",
230        "Up to 10 users",
231        "10GB storage",
232        "Priority support",
233        "API access",
234      ],
235    },
236    {
237      id: "pro",
238      name: "Pro",
239      description: "For growing businesses",
240      monthlyPrice: 99,
241      annualPrice: 990,
242      maxUsers: 50,
243      storage: "100GB",
244      features: [
245        "Full dashboard",
246        "Up to 50 users",
247        "100GB storage",
248        "24/7 support",
249        "Advanced API",
250        "Custom integrations",
251      ],
252    },
253  ];
254
255  const addOns: AddOn[] = [
256    {
257      id: "extra-storage",
258      name: "Extra Storage",
259      description: "Additional 50GB storage",
260      monthlyPrice: 10,
261      annualPrice: 100,
262      category: "Storage",
263    },
264    {
265      id: "advanced-analytics",
266      name: "Advanced Analytics",
267      description: "Detailed reporting and insights",
268      monthlyPrice: 25,
269      annualPrice: 250,
270      category: "Analytics",
271    },
272    {
273      id: "priority-support",
274      name: "Priority Support",
275      description: "24/7 premium support",
276      monthlyPrice: 15,
277      annualPrice: 150,
278      category: "Support",
279    },
280    {
281      id: "api-calls",
282      name: "Extra API Calls",
283      description: "Additional 10K API calls/month",
284      monthlyPrice: 20,
285      annualPrice: 200,
286      category: "API",
287    },
288  ];
289
290  // Calculate current bill
291  const currentBill = useMemo(() => {
292    const selectedPlanData = plans.find((p) => p.id === selectedPlan);
293    if (!selectedPlanData) return [];
294
295    const items: BillItem[] = [
296      {
297        id: selectedPlanData.id,
298        name: selectedPlanData.name + " Plan",
299        type: "plan",
300        quantity: 1,
301        unitPrice:
302          billingCycle === "monthly"
303            ? selectedPlanData.monthlyPrice
304            : selectedPlanData.annualPrice,
305        total:
306          billingCycle === "monthly"
307            ? selectedPlanData.monthlyPrice
308            : selectedPlanData.annualPrice,
309      },
310    ];
311
312    selectedAddOns.forEach((addon) => {
313      items.push({
314        id: addon.id,
315        name: addon.name,
316        type: "addon",
317        quantity: 1,
318        unitPrice:
319          billingCycle === "monthly" ? addon.monthlyPrice : addon.annualPrice,
320        total:
321          billingCycle === "monthly" ? addon.monthlyPrice : addon.annualPrice,
322      });
323    });
324
325    return items;
326  }, [selectedPlan, selectedAddOns, billingCycle, plans]);
327
328  const totalAmount = currentBill.reduce((sum, item) => sum + item.total, 0);
329  const selectedPlanData = plans.find((p) => p.id === selectedPlan);
330
331  return (
332    <div className="min-h-screen bg-background p-4 md:p-6 lg:p-8">
333      <div className="max-w-7xl mx-auto space-y-8">
334        {/* Header */}
335        <div className="space-y-2">
336          <h1 className="text-3xl font-bold text-foreground">
337            Billing & Plans
338          </h1>
339          <p className="text-muted-foreground">
340            Manage your subscription and billing for {customerData.companyName}
341          </p>
342        </div>
343
344        {/* Current Status Cards */}
345        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
346          <Card>
347            <CardContent className="p-6">
348              <div className="flex items-center space-x-2">
349                <CreditCard className="h-5 w-5 text-blue-600" />
350                <div>
351                  <p className="text-sm font-medium text-muted-foreground">
352                    Current Plan
353                  </p>
354                  <p className="text-2xl font-bold text-foreground">
355                    {plans.find((p) => p.id === customerData.currentPlan)
356                      ?.name || "Free"}
357                  </p>
358                </div>
359              </div>
360            </CardContent>
361          </Card>
362
363          <Card>
364            <CardContent className="p-6">
365              <div className="flex items-center space-x-2">
366                <Calendar className="h-5 w-5 text-green-600" />
367                <div>
368                  <p className="text-sm font-medium text-muted-foreground">
369                    Next Billing
370                  </p>
371                  <p className="text-2xl font-bold text-foreground">Feb 15</p>
372                </div>
373              </div>
374            </CardContent>
375          </Card>
376
377          <Card>
378            <CardContent className="p-6">
379              <div className="flex items-center space-x-2">
380                <Users className="h-5 w-5 text-purple-600" />
381                <div>
382                  <p className="text-sm font-medium text-muted-foreground">
383                    Active Users
384                  </p>
385                  <p className="text-2xl font-bold text-foreground">
386                    {customerData.totalUsers}
387                  </p>
388                </div>
389              </div>
390            </CardContent>
391          </Card>
392        </div>
393
394        <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
395          {/* Plans Selection */}
396          <div className="lg:col-span-2 space-y-6">
397            <Card>
398              <CardHeader>
399                <div className="flex items-center justify-between">
400                  <CardTitle>Choose Your Plan</CardTitle>
401                  <div className="flex items-center space-x-2">
402                    <span
403                      className={cn(
404                        "text-sm",
405                        billingCycle === "monthly"
406                          ? "font-medium"
407                          : "text-muted-foreground"
408                      )}
409                    >
410                      Monthly
411                    </span>
412                    <Switch
413                      checked={billingCycle === "annual"}
414                      onCheckedChange={(checked) =>
415                        setBillingCycle(checked ? "annual" : "monthly")
416                      }
417                    />
418                    <span
419                      className={cn(
420                        "text-sm",
421                        billingCycle === "annual"
422                          ? "font-medium"
423                          : "text-muted-foreground"
424                      )}
425                    >
426                      Annual
427                    </span>
428                    {billingCycle === "annual" && (
429                      <Badge variant="secondary" className="text-xs">
430                        Save 20%
431                      </Badge>
432                    )}
433                  </div>
434                </div>
435              </CardHeader>
436              <CardContent className="space-y-4">
437                {plans.map((plan) => (
438                  <div
439                    key={plan.id}
440                    className={cn(
441                      "relative border-2 rounded-lg p-4 cursor-pointer transition-all",
442                      selectedPlan === plan.id
443                        ? "border-primary bg-primary/5"
444                        : "border-border hover:border-primary/50"
445                    )}
446                    onClick={() => setSelectedPlan(plan.id)}
447                  >
448                    {plan.popular && (
449                      <Badge className="absolute -top-2 left-4 bg-primary">
450                        Popular
451                      </Badge>
452                    )}
453                    <div className="flex items-center justify-between">
454                      <div className="space-y-1">
455                        <h3 className="font-semibold text-lg">{plan.name}</h3>
456                        <p className="text-sm text-muted-foreground">
457                          {plan.description}
458                        </p>
459                        <div className="flex items-baseline space-x-1">
460                          <span className="text-2xl font-bold">
461                            $
462                            {billingCycle === "monthly"
463                              ? plan.monthlyPrice
464                              : plan.annualPrice}
465                          </span>
466                          <span className="text-sm text-muted-foreground">
467                            /{billingCycle === "monthly" ? "month" : "year"}
468                          </span>
469                        </div>
470                      </div>
471                      <div className="flex items-center space-x-4">
472                        <div className="text-right text-sm text-muted-foreground">
473                          <div>Up to {plan.maxUsers} users</div>
474                          <div>{plan.storage} storage</div>
475                        </div>
476                        <div
477                          className={cn(
478                            "w-5 h-5 rounded-full border-2 flex items-center justify-center",
479                            selectedPlan === plan.id
480                              ? "border-primary bg-primary"
481                              : "border-muted-foreground"
482                          )}
483                        >
484                          {selectedPlan === plan.id && (
485                            <Check className="w-3 h-3 text-primary-foreground" />
486                          )}
487                        </div>
488                      </div>
489                    </div>
490                    <div className="mt-3 flex flex-wrap gap-2">
491                      {plan.features.map((feature, index) => (
492                        <Badge
493                          key={index}
494                          variant="outline"
495                          className="text-xs"
496                        >
497                          {feature}
498                        </Badge>
499                      ))}
500                    </div>
501                  </div>
502                ))}
503              </CardContent>
504            </Card>
505
506            {/* Add-ons */}
507            <Card>
508              <CardHeader>
509                <CardTitle>Add-ons</CardTitle>
510              </CardHeader>
511              <CardContent>
512                <MultipleSelect
513                  tags={addOns}
514                  onChange={setSelectedAddOns}
515                  defaultValue={[]}
516                />
517              </CardContent>
518            </Card>
519          </div>
520
521          {/* Current Bill Summary */}
522          <div className="space-y-6">
523            <Card>
524              <CardHeader>
525                <CardTitle className="flex items-center space-x-2">
526                  <DollarSign className="h-5 w-5" />
527                  <span>Current Bill</span>
528                </CardTitle>
529              </CardHeader>
530              <CardContent className="space-y-4">
531                {currentBill.map((item) => (
532                  <div key={item.id} className="flex justify-between text-sm">
533                    <div>
534                      <p className="font-medium">{item.name}</p>
535                      <p className="text-muted-foreground">
536                        Qty {item.quantity} • ${item.unitPrice}/
537                        {billingCycle === "monthly" ? "mo" : "yr"}
538                      </p>
539                    </div>
540                    <p className="font-medium">${item.total}</p>
541                  </div>
542                ))}
543
544                <Separator />
545
546                <div className="flex justify-between font-semibold">
547                  <p>Total</p>
548                  <p>
549                    ${totalAmount}/{billingCycle === "monthly" ? "mo" : "yr"}
550                  </p>
551                </div>
552
553                {billingCycle === "annual" && (
554                  <div className="text-xs text-green-600 flex items-center space-x-1">
555                    <TrendingUp className="h-3 w-3" />
556                    <span>
557                      You save ${Math.round(totalAmount * 0.2)} annually
558                    </span>
559                  </div>
560                )}
561
562                <Button className="w-full" size="lg">
563                  Update Subscription
564                </Button>
565
566                <div className="text-xs text-muted-foreground text-center">
567                  Changes will be applied to your next billing cycle
568                </div>
569              </CardContent>
570            </Card>
571
572            {/* Usage Warning */}
573            {selectedPlanData &&
574              customerData.totalUsers > (selectedPlanData.maxUsers || 0) && (
575                <Card className="border-orange-200 bg-orange-50">
576                  <CardContent className="p-4">
577                    <div className="flex items-start space-x-2">
578                      <AlertCircle className="h-5 w-5 text-orange-600 mt-0.5" />
579                      <div className="space-y-1">
580                        <p className="text-sm font-medium text-orange-800">
581                          User Limit Exceeded
582                        </p>
583                        <p className="text-xs text-orange-700">
584                          You have {customerData.totalUsers} users but your
585                          selected plan only supports{" "}
586                          {selectedPlanData.maxUsers}. Consider upgrading to
587                          avoid service interruption.
588                        </p>
589                      </div>
590                    </div>
591                  </CardContent>
592                </Card>
593              )}
594          </div>
595        </div>
596      </div>
597    </div>
598  );
599};
600
601export default CustomerAdminDashboard;

Dependencies

External Libraries

framer-motionlucide-reactreact

Shadcn/UI Components

badgebuttoncardseparatorswitch

Local Components

/lib/utils

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

For questions about licensing, please contact the project maintainers.