DEV Community

Cover image for What I learned building an after-tax raise calculator
Mark
Mark

Posted on

What I learned building an after-tax raise calculator

I built a pay raise calculator because I got a raise, multiplied my salary by 1.05, and then watched my actual paycheck go up by a lot less than I expected. The gap between "5% raise" and "the extra money that shows up" turned out to be wide enough that I wanted to understand it properly — and once I started writing the code, "what's my raise worth?" split into three completely different questions.

This is a write-up of the small TypeScript engine behind it, and the three gotchas that made the math less obvious than salary * 1.05.

Question 1: the gross number (the easy one)

This is the only part that is salary * 1.05:

const newSalary = currentSalary * (1 + raisePct / 100);
Enter fullscreen mode Exit fullscreen mode

A $80,000 salary with a 5% raise is $84,000. A $4,000 raise. Done. This is the number everyone quotes, and it's the least useful one, because you never see $4,000.

Question 2: the after-tax number (where the marginal bracket bites)

The first thing people get wrong — including me, before I wrote this — is assuming a raise is taxed at your average rate. It isn't. A raise lands entirely on top of your existing income, so it's taxed at your marginal rate: the bracket your last dollar falls into.

The other thing people get wrong is the opposite, and it's a myth I wanted the tool to kill: "a raise can bump me into a higher bracket and leave me with less money." That can't happen with progressive brackets — only the dollars inside the higher band are taxed at the higher rate. The brackets are bands, not switches.

So the engine computes total tax at the old and new salary and just subtracts:

type Bracket = { upTo: number; rate: number };

function taxFromBrackets(taxable: number, brackets: Bracket[]): number {
  let tax = 0;
  let prevCap = 0;
  for (const { upTo, rate } of brackets) {
    const band = Math.min(taxable, upTo) - prevCap;
    if (band <= 0) break;
    tax += band * rate;   // only THIS band's dollars get THIS rate
    prevCap = upTo;
  }
  return tax;
}

function netRaise(currentGross: number, newGross: number): number {
  const taxBefore = totalTax(currentGross); // federal + FICA + state
  const taxAfter  = totalTax(newGross);
  return (newGross - currentGross) - (taxAfter - taxBefore);
}
Enter fullscreen mode Exit fullscreen mode

The federal part is easy once you have taxFromBrackets. FICA is where the cliffs live, and it's the part most back-of-envelope calculators skip:

function calcFica(gross: number): number {
  // Social Security: 6.2%, but ONLY up to the annual wage base.
  const ss = Math.min(gross, SS_WAGE_BASE) * 0.062;

  // Medicare: 1.45% on everything...
  let medicare = gross * 0.0145;
  // ...plus a 0.9% surtax on dollars above a high threshold.
  if (gross > ADDITIONAL_MEDICARE_THRESHOLD) {
    medicare += (gross - ADDITIONAL_MEDICARE_THRESHOLD) * 0.009;
  }
  return ss + medicare;
}
Enter fullscreen mode Exit fullscreen mode

That Math.min(gross, SS_WAGE_BASE) is the interesting line. Social Security stops being withheld once you cross the wage base for the year — so the same 5% raise keeps a wildly different fraction of itself depending on where you sit relative to that cap. A raise that straddles the wage base is partly free of the 6.2%; a raise entirely below it isn't. A flat "multiply by 0.92 for taxes" can't express that.

Worked example, $80,000 → $84,000, single filer, no state tax:

Amount
Gross raise $4,000
Extra federal tax (22% marginal band) −$880
Extra FICA (6.2% + 1.45%) −$306
Net raise $2,814

You keep about 70% of it. The headline "$4,000" was never going to hit your account. And note the federal hit is 22% even though this person's effective federal rate is around 11% — that's the marginal-vs-average gap in one number.

Question 3: the real number (inflation eats the rest)

Here's the question that actually matters and almost no calculator answers: did your purchasing power go up?

If prices rose 3.3% over the year and your raise was 5%, you are not 5% richer and you're not even 1.7% richer (the naive subtraction). The correct relationship is multiplicative, because you're deflating next year's dollars back to today's:

function realRaise(nominalPct: number, inflationPct: number): number {
  const n = nominalPct / 100;
  const i = inflationPct / 100;
  return ((1 + n) / (1 + i) - 1) * 100;
}

realRaise(5, 3.3); // → 1.645...  not 1.7
Enter fullscreen mode Exit fullscreen mode

So the $4,000-that's-really-$2,814 raise is, in purchasing-power terms, about a 1.6% real raise. And the uncomfortable corollary the tool makes very visible: any nominal raise below the inflation rate is a real pay cut, even though the number on the letter is positive. A "3% raise" in a 3.3% year means you can buy less than you could last year.

The architecture decision that mattered most: one source of truth

The boring part turned out to be the most important. Early on I had tax constants and "current inflation" hardcoded into page copy — $168,600 here, "3.2%" in one blog post, "3.3%" in another. They drifted. A site whose entire pitch is accurate numbers had inconsistent numbers, which is the worst possible failure mode.

The fix was to make every displayed figure — including the ones baked into prose and reference tables — derive from a single constants module, and to compute the example tables at build time from the same engine that powers the live calculator:

// data/tax-2026.ts — the only place a tax/inflation number is written down,
// each one cross-checked against two independent sources before it lands.
export const CPI_U_LATEST = { rate: 0.033, periodLabel: "12 months ending March 2026" };
export const SS_WAGE_BASE = 184_500;
// ...brackets, deductions, FICA rates...
Enter fullscreen mode Exit fullscreen mode

Now a reference table like "what a 3% / 5% / 7% raise nets at $60k / $80k / $100k" isn't typed out by hand — it's a .map() over the same calcRaiseAfterTax the interactive tool calls. The numbers in the article body cannot disagree with the calculator, because they're the same function. Updating for next tax year is a one-line edit to the constants, and the whole site re-derives. (It's a Next.js static export, so all of this runs at build time and ships as plain HTML.)

If I'd known one thing starting out, it's this: for any "calculator" content site, the moment a number exists in two places it's already wrong. Treat the constants as code, compute the prose from them, and never let a human re-type a figure that a function can produce.

Takeaways

  • A raise is taxed at your marginal rate, not your average — but progressive brackets mean a raise can never lower your take-home.
  • FICA wage-base and surtax thresholds create cliffs that flat "multiply by 0.9" estimates silently miss.
  • Real (inflation-adjusted) raise is (1+n)/(1+i) − 1, not n − i, and it's the only one of the three numbers that tells you whether you're actually better off.
  • Derive every displayed figure from one constants module. If a number lives in two files, you have a bug waiting to surface.

The live version with the interactive math is at raise-calculator.com if you want to plug in your own numbers — but honestly the three formulas above are the whole thing. Happy to talk through the bracket edge cases in the comments.

Top comments (0)