Budget variance
How the budget moved from plan to actual.
People budget variance
FY2026 plan $31.2M · forecast $31.1M · -$100K against plan
The forecast is $31.1M, $100K under the $31.2M plan. Salaries ran $0.9M over, offset by $1.4M saved on roles that opened late.
- Increase
- Decrease
- Total
View data as a table
| Step | Type | Change | Total |
|---|---|---|---|
| Plan | Total | $31.2M | |
| Salaries | Increase | +$900K | $32.1M |
| New-hire salaries | Decrease | -$1.4M | $30.7M |
| Contractors | Increase | +$600K | $31.3M |
| Benefits | Increase | +$300K | $31.6M |
| Training | Decrease | -$200K | $31.4M |
| Recruiting fees | Decrease | -$300K | $31.1M |
| Forecast | Total | $31.1M |
Explain a line
Budget line
Market adjustment for engineering in April.
Salaries, FY2026
- Plan
- $21.4M
- Forecast
- $22.3M
- Variance
- +$900K
Over plan
budget-variance.tsx
"use client";
import { useState } from "react";
import { ChartTooltip, Select, SelectOption, WaterfallChart } from "@nexera-ui/react";
// FY2026 people budget in thousands of US dollars: plan $31.2M, forecast $31.1M.
const LINES = [
{
id: "salaries",
label: "Salaries",
plan: 21400,
actual: 22300,
note: "Market adjustment for engineering in April.",
},
{
id: "hiring",
label: "New-hire salaries",
plan: 2600,
actual: 1200,
note: "Six roles opened later than planned.",
},
{
id: "contractors",
label: "Contractors",
plan: 1800,
actual: 2400,
note: "Covered the data migration while roles were open.",
},
{
id: "benefits",
label: "Benefits",
plan: 3900,
actual: 4200,
note: "Health insurance renewal came in 8% higher.",
},
{
id: "training",
label: "Training",
plan: 600,
actual: 400,
note: "Leadership programme moved to Q1 2027.",
},
{
id: "recruiting",
label: "Recruiting fees",
plan: 900,
actual: 600,
note: "More hires through referrals, fewer through agencies.",
},
];
const PLAN = LINES.reduce((total, line) => total + line.plan, 0);
const ACTUAL = LINES.reduce((total, line) => total + line.actual, 0);
const usd = (thousands: number, sign = false) =>
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
maximumFractionDigits: 1,
signDisplay: sign ? "exceptZero" : "auto",
}).format(thousands * 1000);
/** Budget variance: how the FY2026 people budget moved from plan to forecast, with the reason behind each line. */
export default function BudgetVariance() {
const [selected, setSelected] = useState<string | number | null>("salaries");
const line = LINES.find((item) => item.id === selected) ?? LINES[0];
if (!line) return null;
const variance = line.actual - line.plan;
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-6xl gap-6">
<header className="grid gap-1">
<h2 className="text-heading-h2 text-primary">People budget variance</h2>
<p className="text-body-default text-secondary">
FY2026 plan {usd(PLAN)} · forecast {usd(ACTUAL)} · {usd(ACTUAL - PLAN, true)} against
plan
</p>
</header>
<WaterfallChart
title="From plan to forecast"
description="Fiscal year 2026, all departments"
summary={`The forecast is ${usd(ACTUAL)}, ${usd(PLAN - ACTUAL)} under the ${usd(PLAN)} plan. Salaries ran $0.9M over, offset by $1.4M saved on roles that opened late.`}
steps={[
{ label: "Plan", value: PLAN, kind: "total" },
...LINES.map((item) => ({ label: item.label, value: item.actual - item.plan })),
{ label: "Forecast", kind: "total", isHighlighted: true },
]}
formatValue={(value) => usd(value)}
formatAxisValue={(value) => usd(value)}
height={280}
/>
<section
aria-labelledby="variance-detail"
className="grid gap-4 rounded-lg border border-default bg-surface p-4 @3xl:grid-cols-2 @3xl:p-6"
>
<div className="grid content-start gap-3">
<h3 id="variance-detail" className="text-heading-h4 text-primary">
Explain a line
</h3>
<Select label="Budget line" value={selected} onChange={setSelected}>
{LINES.map((item) => (
<SelectOption key={item.id} id={item.id} label={item.label} />
))}
</Select>
<p className="text-body-small text-secondary">{line.note}</p>
</div>
<ChartTooltip
className="self-start"
title={`${line.label}, FY2026`}
rows={[
{ label: "Plan", value: usd(line.plan), seriesIndex: 1, shape: "square" },
{ label: "Forecast", value: usd(line.actual), seriesIndex: 0, shape: "square" },
{ label: "Variance", value: usd(variance, true) },
]}
>
{variance > 0 ? "Over plan" : "Under plan"}
</ChartTooltip>
</section>
</div>
</div>
);
}
More dashboards and analytics blocks
- KPI overviewFour headline metrics with trend and change.
- Sales dashboardRevenue over time against target.
- People analyticsHeadcount by team, type and tenure.
- Recruitment funnelApplicants at each stage and the drop-off between.
- Engagement survey resultseNPS score and check-in patterns across the week.
- Skills comparisonOne person against the team average.
- Live status boardReal-time counts with tiny trends.
- Chart statesLoading, empty and error versions of any chart.