An ultra-fast, tiny TypeScript implementation of common UK Income Tax & National Insurance calculations. See it in action on SavingTool.co.uk.
This library makes it easy to calculate, based on a PAYE taxable salary:
- Personal Allowance
- Income Tax
- Employee National Insurance Contributions (Class 1, Category A only)
- Student Loan Repayments (Plans 1, 2, 4, 5 or postgrad)
- Pension annual allowance, including pension tapering
Multiple versions of the HMRC rates can be supported, although only the follwing years have been implemented:
England/NI/Wales:
- 2025/26 (default)
- 2024/25
- 2023/24
- 2022/23
Scotland:
- 2025/26 (default)
- 2024/25
Works in all modern browsers and Node.js (LTS recommended).
Run: yarn add @saving-tool/hmrc-income-tax (or npm install @saving-tool/hmrc-income-tax)
country is an optional input for all APIs: "England/NI/Wales" | "Scotland". If not provided, the default is "England/NI/Wales".
Note that taxYear is an optional input to select which tax year rates should be used (default is "2025/26").
Calculates an individual's personal allowance for a tax year, single amount.
calculatePersonalAllowance({
taxYear?: TaxYear,
country?: Country,
taxableAnnualIncome: number
}) => number;Calculates the income tax due in a tax year on an individual's taxable income
calculateIncomeTax({
taxYear?: TaxYear;
country?: Country,
personalAllowance?: number,
taxableAnnualIncome?: number
}) => EnglishIncomeTax | ScottishIncomeTax;New in v1.3.0: Cumulative PAYE Mode
For variable monthly income scenarios (e.g., NHS workers with variable pay), you can now use cumulative PAYE calculation mode:
calculateIncomeTax({
taxYear?: TaxYear;
country?: Country,
personalAllowance?: number, // Optional: if provided, uses this value; otherwise auto-calculates with tapering
cumulativePaye: {
monthNumber: number; // 1-12, which month of the tax year
cumulativeGrossIncome: number; // Total gross income to date in the tax year
cumulativeTaxPaid: number; // Total tax already paid to date in the tax year
}
}) => EnglishIncomeTax | ScottishIncomeTax;Important: In cumulative PAYE mode, the function returns the tax due for that specific month only, not the total cumulative tax. This represents what should be deducted from the current month's payslip.
Example 1: £10,000 income in month 1, then £0 for rest of year
// Month 1: £10,000 gross income
const month1Tax = calculateIncomeTax({
taxYear: "2024/25",
cumulativePaye: {
monthNumber: 1,
cumulativeGrossIncome: 10_000,
cumulativeTaxPaid: 0,
},
});
// Returns: { total: 1790.5, ... } - deduct £1,790.50 from month 1 payslip
// Month 2: No additional income
const month2Tax = calculateIncomeTax({
taxYear: "2024/25",
cumulativePaye: {
monthNumber: 2,
cumulativeGrossIncome: 10_000, // Still £10k total
cumulativeTaxPaid: 1790.5, // Tax paid in month 1
},
});
// Returns: { total: 0, ... } - deduct £0 from month 2 payslip (no tax due this month)Example 2: Variable high income with personal allowance tapering
// Month 1: £60,000 gross income
const month1Tax = calculateIncomeTax({
taxYear: "2024/25",
cumulativePaye: {
monthNumber: 1,
cumulativeGrossIncome: 60_000,
cumulativeTaxPaid: 0,
},
});
// Returns: { total: 16041, ... } - deduct £16,041 from month 1 payslip
// Month 6: Additional £50k earned (£110k total, crossing into PA tapering)
const month6Tax = calculateIncomeTax({
taxYear: "2024/25",
cumulativePaye: {
monthNumber: 6,
cumulativeGrossIncome: 110_000, // £60k + £50k
cumulativeTaxPaid: 16041, // Tax paid in month 1
},
});
// Returns: { total: 17391, ... } - deduct £17,391 from month 6 payslipThis cumulative mode implements HMRC PAYE rules where personal allowances are pro-rated monthly and handles high earner tapering, avoiding the over-taxation that occurs when simply annualizing variable monthly income.
Calculates the National Insurance contributions due in a tax year on an individual's taxable income, single amount. Note: only supports class 1, category A.
calculateEmployeeNationalInsurance({
taxYear?: TaxYear,
country?: Country,
taxableAnnualIncome: number
}) => number;Calculates the student loan repayments due in a tax year on an individual's taxable income, single amount.
calculateStudentLoanRepayments({
taxYear?: TaxYear,
country?: Country,
taxableAnnualIncome: number,
studentLoanPlanNo: 1 | 2 | 4 | 5 | 'postgrad'
}) => number;Returns an underlying static set of HMRC rates for a given tax year. This is useful for doing your own arbitrary calculations.
getHmrcRates({
taxYear?: TaxYear,
country?: Country
}) => EnglishTaxRates | ScottishTaxRates;Returns an object containing an annual allowance information for pension contributions. Note that pension tapering calculations are quite complex. You can also refer to the tests with various examples.
"Personally paid" pension contributions means you paid from your bank account direct to a pension i.e. not through work.
For employee pension contributions:
- Use
employeeDcPensionContributionsfor salary sacrifice contributions (post-2015 schemes*) - Use
retrospectivePensionPaymentsTaxRelieffor salary sacrifice contributions (pre-2015 schemes*) - Use
retrospectivePensionPaymentsTaxRelieffor personally paid or other relief-at-source contributions
* The rules changed for salary sacrifice schemes set up on or after 9th July 2015
calculatePensionAnnualAllowance({
taxYear?: TaxYear;
totalAnnualIncome: number; // Note: include any salary sacrificed income, plus investent income, but do not include employer contributions, or relief-at-source contributions such as ones personally paid
retrospectivePensionPaymentsTaxRelief?: number; // Relief-at-source pension contributions such as ones personally paid
employeeDcPensionContributions?: number;
employerDcPensionContributions?: number;
lumpSumDeathBenefits?: number;
}) => {
adjustedIncome: number;
thresholdIncome: number;
reduction: number;
allowance: number;
};Note that this implementation does not yet support the following:
- Use of carry-forward from previous years' allowances
- Accounting for DB (Defined Benefit) pensions
- Paying into overseas pensions
DISCLAIMER: These examples were written against the 2022/23 England/Wales/NI HMRC rates, hence will output different results vs today's HMRC rates.
Mark S. of MDR earns £55,000. His employer contributes 6% to his pension, but also matches up to another 2%. Mark contributes 2% via salary sacrafice to get the matching. Therefore, Mark's taxable income is £53,900. He has £19,000 of outstanding student loan debt, and is on Plan 1.
import {
calculatePersonalAllowance,
calculateIncomeTax,
calculateEmployeeNationalInsurance,
calculateStudentLoanRepayments,
} from "@saving-tool/hmrc-income-tax";
const taxYear = "2022/23";
// Mark S.
const taxableAnnualIncome = 53_900;
const personalAllowance = calculatePersonalAllowance({
taxableAnnualIncome,
taxYear,
});
// => 12570
const incomeTax = calculateIncomeTax({
personalAllowance,
taxableAnnualIncome,
taxYear,
});
const { total } = incomeTax;
// => 8992
const nationalInsuranceContributions = calculateEmployeeNationalInsurance({
taxableAnnualIncome,
taxYear,
});
// => 5471
const studentLoanRepayments = calculateStudentLoanRepayments({
taxableAnnualIncome,
studentLoanPlanNo: 1,
taxYear,
});
// => 3162
// Do whatever you want, e.g. calculate the take-home pay
const takeHome =
taxableAnnualIncome -
totalIncomeTax -
nationalInsuranceContributions -
studentLoanRepayments;
// => 36275Irv B. of MDR earns £160,000. His employer contributes some amount to his pension, but he contributes nothing. He has no student loan.
import {
calculatePersonalAllowance,
calculateIncomeTax,
calculateEmployeeNationalInsurance,
} from "@saving-tool/hmrc-income-tax";
const taxYear = "2022/23";
// Irv B.
const taxableAnnualIncome = 160_000;
const personalAllowance = calculatePersonalAllowance({
taxableAnnualIncome,
taxYear,
});
// => 0
const incomeTax = calculateIncomeTax({
personalAllowance,
taxableAnnualIncome,
taxYear,
});
const { total } = incomeTax;
// => 57589
const nationalInsuranceContributions = calculateEmployeeNationalInsurance({
taxableAnnualIncome,
taxYear,
});
// => 8919
// Do whatever you want, e.g. calculate the take-home pay
const takeHome =
taxableAnnualIncome - totalIncomeTax - nationalInsuranceContributions;
// => 93492It's important to understand that in most cases this library is expecting taxable income (appropriate API naming aims to make this clear). Any salary sacrafice mechanisms should be applied before these calculations, and the appropriate taxable amount used when calling this library.
A formatter/rounder function is not included as to separate that concern from the raw tax calculations. Your application may want to apply it's own rounding and formatting logic.
Example roll-your-own formatter using the Intl API (similar to what SavingTool.co.uk uses):
const gbpFormatter = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
// Rounds an amount of GBP to the nearest pound and formats it
// `amount` can be a long number e.g. 548.729345847 => £549
export const roundAndFormatGbp = (amount: number) => {
return formatGbp(Math.round(amount));
};