Budget vs Actual Variance
Budget-vs-actual variance by line item with favorability-aware coloring and an auto-generated variance narrative. The FP&A starting point.
Budget vs actual · Q1 2026
$4.6M
-$100k vs plan · -2.1% favorable
- Budget
- Actual
Opex spend came in 2.1% under budget in Q1 2026 — $100k favorable.
| Line | Budget | Actual | Variance |
|---|---|---|---|
| Sales | $1.64M | $1.68M | $40k |
| Marketing | $1.1M | $920k | -$180k |
| R&D | $1.4M | $1.42M | $20k |
| G&A | $560k | $580k | $20k |
Acme GL · updated 2:18pm
Demo data is illustrative. Replace with your own typed data prop.
How it computes
Budget-vs-actual variance with favorability that flips polarity for cost vs revenue accounts.
variance = actual − budget; variancePct = budget = 0 ? 0 : variance / budget; favorable = costAccounts ? variance ≤ 0 : variance ≥ 0Assumptions
- The costAccounts toggle decides polarity — under-budget is favorable for costs, over-budget for revenue.
- Totals are simple sums of the line actuals and budgets.
- The top driver is the single largest unfavorable variance by absolute dollars.
Honest about
- Variance % is 0 — not ∞ — when budget = 0, so a $0-budget line with real spend reads as 0%; check the dollar column.
- Lines rank by absolute dollars, so a large line dominates even when a small line missed by a wider percentage.
- Favorability is an explicit toggle (defaults to revenue polarity), never inferred from the numbers.
Use it with your data
Data contract
Passed via the data prop — an array of objects: validated at runtime, so a bad shape degrades to the error state rather than crashing.
| Field | Type | Required |
|---|---|---|
| label | string | yes |
| actual | number | yes |
| budget | number | yes |
Static props
import { FpaVarianceBvA } from "@/components/blocks/fpa-variance-bvA/fpa-variance-bvA";
<FpaVarianceBvA data={myData} />Client fetch (SWR)
const { data, isLoading, error } = useSWR("/api/metric", fetcher);
// route isLoading/error through <ChartStates>, then:
<FpaVarianceBvA data={data} />Server component (RSC)
const data = await fetch(url, {
next: { revalidate: 3600 },
}).then((r) => r.json());
<FpaVarianceBvA data={data} />Map arbitrary rows
import { mapRows } from "@/lib/sextant/column-map";
const out = mapRows(rows, { /* contractKey: "yourColumn" */ }, dataSchema);
if (out.ok) <FpaVarianceBvA data={out.data} />;Make it yours
Every customization below is an optional prop — omit them all and you get the defaults shown above. Recolor, reformat, restyle, or drop the card chrome without forking the component.
Recolor or rename a series (budget, actual)
<FpaVarianceBvA
series={{ budget: { color: "var(--chart-3)", label: "…" } }}
/>Format numbers (currency / locale)
const eur = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
notation: "compact",
});
<FpaVarianceBvA valueFormatter={(n) => eur.format(n)} />Replace or hide the narrative
<FpaVarianceBvA narrative="Your own one-liner." />
// …or hide it entirely:
<FpaVarianceBvA narrative={false} />Restyle any region
<FpaVarianceBvA
className="max-w-xl"
classNames={{ body: "bg-muted/20" }}
/>Drop the card chrome (headless plot)
import { FpaVarianceBvAPlot } from "@/components/blocks/fpa-variance-bvA/fpa-variance-bvA";
// Just the chart body — bring your own card/layout:
<FpaVarianceBvAPlot data={data} config={config} />