Vector Motion
Finance

Budget vs Actual Card - Financial Planning

Compare budget forecasts with actual spending. Monitor budget variance and spending efficiency.

Finance Budget vs Actual Card

The Finance Budget vs Actual Card helps teams track spending against budgets, identifying variances and enabling better financial control.

Preview

Installation

npx shadcn@latest add https://vectormotion.vercel.app/registry/finance-budget-vs-actual-card.json
Finance Budget vs Actual Card
'use client';'use client';import React from 'react';import { Calculator } from 'lucide-react';import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Cell } from 'recharts';import { motion } from 'motion/react';import { clsx, type ClassValue } from "clsx"import { twMerge } from "tailwind-merge"function cn(...inputs: ClassValue[]) {  return twMerge(clsx(inputs))}interface DataItem {  category: string;  budget: number;  actual: number;}interface BudgetVsActualCardProps {  isInteractive?: boolean;  className?: string;  title?: string;  description?: string;  dateLabel?: string;  data?: DataItem[];}const DEFAULT_TITLE = "Budget vs Actual";const DEFAULT_DESCRIPTION = "Departmental performance";const DEFAULT_DATE_LABEL = "Oct 2023";const DEFAULT_DATA: DataItem[] = [  { category: 'Marketing', budget: 5000, actual: 4200 },  { category: 'R&D', budget: 8000, actual: 8500 }, // Over budget  { category: 'Ops', budget: 3000, actual: 2800 },  { category: 'Sales', budget: 6000, actual: 5500 },];export const BudgetVsActualCard: React.FC<BudgetVsActualCardProps> = ({  isInteractive = true,  className = "",  title = DEFAULT_TITLE,  description = DEFAULT_DESCRIPTION,  dateLabel = DEFAULT_DATE_LABEL,  data = DEFAULT_DATA,}) => {  const index = 30;  return (    <motion.div      layoutId={isInteractive ? `card-${index}-${title}` : undefined}      transition={{ duration: 0.4, ease: "easeOut" }}      className={cn(        "relative overflow-hidden rounded-xl border border-border bg-card text-card-foreground p-6 shadow-sm transition-all flex flex-col group",        isInteractive ? "cursor-pointer hover:border-zinc-300 dark:hover:border-zinc-700" : "",        className      )}    >      <div className="mb-4 flex items-start justify-between relative z-10">        <div>          <h3 className="font-semibold text-lg tracking-tight text-foreground">            {title}          </h3>          {description && (            <p className="text-sm text-muted-foreground mt-1">              {description}            </p>          )}        </div>        <div className="rounded-full bg-zinc-100 dark:bg-zinc-800 p-2 text-foreground flex items-center justify-center">          <Calculator className="h-5 w-5" />        </div>      </div>      <div className="relative z-10 flex-1">        <div className="h-[200px] w-full -ml-4">          <ResponsiveContainer width="100%" height="100%">            <BarChart data={data} layout="vertical" barGap={2} barCategoryGap={20}>              <XAxis type="number" hide />              <YAxis                dataKey="category"                type="category"                axisLine={false}                tickLine={false}                width={70}                tick={{ fill: '#71717a', fontSize: 12 }}              />              <Tooltip                cursor={{ fill: 'transparent' }}                contentStyle={{ backgroundColor: 'var(--tooltip-bg)', borderRadius: '8px', border: 'none', color: 'var(--tooltip-text)', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}              />              <Bar                dataKey="budget"                fill="#e4e4e7"                radius={[0, 4, 4, 0]}                barSize={12}                animationDuration={1500}                className="fill-zinc-200 dark:fill-zinc-800"              />              <Bar                dataKey="actual"                radius={[0, 4, 4, 0]}                barSize={12}                animationDuration={1500}                animationBegin={300}              >                {data.map((entry, index) => (                  <Cell                    key={`cell-${index}`}                    fill={entry.actual > entry.budget ? '#ef4444' : '#10b981'} // Red if over, Green if under                  />                ))}              </Bar>            </BarChart>          </ResponsiveContainer>        </div>        <motion.div          initial={{ opacity: 0 }}          animate={{ opacity: 1 }}          transition={{ delay: 1 }}          className="mt-2 flex justify-between items-center px-1"        >          <div className="flex items-center gap-2 text-xs text-muted-foreground">            <div className="w-2 h-2 rounded-full bg-zinc-200 dark:bg-zinc-800"></div>            <span>Budget</span>            <div className="w-2 h-2 rounded-full bg-emerald-500 ml-2"></div>            <span>Actual (Safe)</span>            <div className="w-2 h-2 rounded-full bg-red-500 ml-2"></div>            <span>Over</span>          </div>          <span className="text-xs font-medium text-foreground">{dateLabel}</span>        </motion.div>      </div>    </motion.div>  );};

Props

Prop

Type