A calculator and tax documents beside a laptop
Concept visualization: a calculator and tax documents on a desk beside a laptop showing a tax calculator interface. Generated illustration, not a photo of the actual project.
2023 Application

Overview

TaxGlobe is a client-side web application that calculates Canadian income tax for a given gross income and province. The user enters their annual income and selects a province from a dropdown; the application calculates both federal and provincial taxes by iterating through 2023 marginal tax bracket data, producing a full breakdown showing: tax owed in each bracket, total federal tax, total provincial tax, combined tax, effective tax rate (as a percentage of gross income), and net take-home income. All calculation logic runs in the browser with no server round-trip. Results update in real time as the user types or changes the province selection, with floating-point currency values rounded correctly to two decimal places using Math.round arithmetic to avoid the binary representation errors that affect naive floating-point addition.

Architecture

graph TD subgraph Input["User Input"] A["Income Input\nnumber field"] B["Province Select\ndropdown"] end subgraph Engine["JavaScript Tax Engine"] C["Federal Bracket Loop\n2023 CRA brackets\nMarginal rate accumulation"] D["Provincial Bracket Loop\nPer-province bracket data\nMarginal rate accumulation"] E["Totals Calculator\nTotal tax · Effective rate\nNet income"] end subgraph Output["Results Display"] F["Federal Breakdown Table\nBracket · Rate · Tax Owed"] G["Provincial Breakdown Table\nBracket · Rate · Tax Owed"] H["Summary Card\nTotal Tax · Effective Rate · Net Income"] end A --> C A --> D B --> D C --> E D --> E C --> F D --> G E --> H style A fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style B fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style C fill:#181818,stroke:#1e1e1e,color:#888 style D fill:#181818,stroke:#1e1e1e,color:#888 style E fill:#181818,stroke:#1e1e1e,color:#888 style F fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0 style G fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0 style H fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0

Tech Stack

  • Vanilla JavaScript, Complete tax calculation engine; bracket data encoded as JS objects; real-time DOM updates via addEventListener('input')
  • 2023 Canadian Tax Bracket Data, Federal CRA brackets and the provincial bracket tables encoded as arrays of { min, max, rate } objects
  • HTML5, Form with type="number" income input and province <select>; results rendered into a structured table and summary card
  • CSS3, Clean card layout for results; responsive grid for the breakdown tables; colour-coded effective rate display

Build Process

1
Research & Encode 2023 Tax Bracket Data

The 2023 federal and provincial tax brackets were researched from CRA publications and encoded as JavaScript data objects. Each bracket entry specifies the lower bound, upper bound (or Infinity for the top bracket), and the marginal rate as a decimal.

// taxData.js - 2023 Canadian federal brackets
const federalBrackets = [
  { min: 0,       max: 53359,   rate: 0.15   },
  { min: 53359,   max: 106717,  rate: 0.205  },
  { min: 106717,  max: 165430,  rate: 0.26   },
  { min: 165430,  max: 235675,  rate: 0.29   },
  { min: 235675,  max: Infinity, rate: 0.33  },
];

// Ontario provincial brackets (example)
const ontarioBrackets = [
  { min: 0,       max: 49231,   rate: 0.0505 },
  { min: 49231,   max: 98463,   rate: 0.0915 },
  { min: 98463,   max: 150000,  rate: 0.1116 },
  { min: 150000,  max: 220000,  rate: 0.1216 },
  { min: 220000,  max: Infinity, rate: 0.1316 },
];
2
Marginal Tax Calculation Engine

The core calculation function iterates through the bracket array. For each bracket, it computes the taxable amount within that bracket, the lesser of the income above the bracket floor and the bracket width, and multiplies by the marginal rate. Accumulated totals across all brackets give the total tax for that bracket set.

function calcTax(income, brackets) {
  let totalTax = 0;
  const breakdown = [];

  for (const bracket of brackets) {
    if (income <= bracket.min) break;
    const taxableInBracket = Math.min(income, bracket.max) - bracket.min;
    const taxInBracket = taxableInBracket * bracket.rate;
    totalTax += taxInBracket;
    breakdown.push({
      min:    bracket.min,
      max:    bracket.max,
      rate:   bracket.rate,
      taxable: taxableInBracket,
      tax:    taxInBracket,
    });
  }

  return { totalTax, breakdown };
}
3
Effective Rate & Net Income Calculation

After running both the federal and provincial calculations, totals are combined and derived figures computed. Floating-point values are rounded to cents using Math.round(value * 100) / 100 before display, preventing binary representation artefacts from showing incorrect cent values in the output.

function calcSummary(income, province) {
  const fed   = calcTax(income, federalBrackets);
  const prov  = calcTax(income, provinceBrackets[province]);
  const total = fed.totalTax + prov.totalTax;
  const round = v => Math.round(v * 100) / 100;
  return {
    federalTax:    round(fed.totalTax),
    provincialTax: round(prov.totalTax),
    totalTax:      round(total),
    effectiveRate: round((total / income) * 100),
    netIncome:     round(income - total),
    fedBreakdown:  fed.breakdown,
    provBreakdown: prov.breakdown,
  };
}
4
HTML Form: Income Input & Province Select

The form uses a type="number" input with min="0" and step="1" for the income field. The province dropdown is populated from the keys of the bracket data object, so adding a new province's data automatically adds it to the dropdown without modifying the HTML. Both inputs use required for HTML5 validation.

5
CSS Results Card & Breakdown Table

Results are displayed in two sections: a summary card showing the headline figures (total tax, effective rate, net income) and two collapsible breakdown tables showing the per-bracket calculation for federal and provincial tax respectively. The effective rate is colour-coded, green for rates under 20%, amber for 20 to 30%, red for above 30%.

6
Real-Time Calculation on Input

Both the income input and province select listen for input events. On each event, the calculation runs and the DOM is updated immediately, no submit button required. Input debouncing with a short timeout prevents excessive recalculation during rapid typing.

let debounceTimer;
[incomeInput, provinceSelect].forEach(el => {
  el.addEventListener('input', () => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      const income = parseFloat(incomeInput.value);
      const province = provinceSelect.value;
      if (!isNaN(income) && income > 0 && province) {
        renderResults(calcSummary(income, province));
      }
    }, 150);
  });
});

Calculation Flow

flowchart LR A["User Enters Income\n+ Selects Province"] --> B["input event fires\n150ms debounce"] B --> C["calcTax(income, federalBrackets)\nIterate brackets · Accumulate"] B --> D["calcTax(income, provinceBrackets[province])\nIterate brackets · Accumulate"] C --> E["Federal Total Tax\n+ Breakdown Array"] D --> F["Provincial Total Tax\n+ Breakdown Array"] E --> G["calcSummary()\nCombine totals\nMath.round to cents"] F --> G G --> H["renderResults()\nUpdate DOM"] H --> I["Summary Card\nTotal · Effective Rate · Net"] H --> J["Federal Breakdown Table\nPer-bracket rows"] H --> K["Provincial Breakdown Table\nPer-bracket rows"] style A fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style B fill:#181818,stroke:#1e1e1e,color:#888 style C fill:#181818,stroke:#1e1e1e,color:#888 style D fill:#181818,stroke:#1e1e1e,color:#888 style E fill:#181818,stroke:#1e1e1e,color:#888 style F fill:#181818,stroke:#1e1e1e,color:#888 style G fill:#181818,stroke:#1e1e1e,color:#888 style H fill:#181818,stroke:#1e1e1e,color:#888 style I fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0 style J fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0 style K fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0

Challenges & Solutions

Floating-Point Precision in Currency Calculations. JavaScript's IEEE 754 floating-point arithmetic causes notorious precision errors in currency calculations. For example, 0.1 + 0.2 produces 0.30000000000000004 in JavaScript. In a tax calculator, these errors accumulate across bracket additions and can produce results like $15,234.999999 instead of $15,235.00. The solution was to apply Math.round(value * 100) / 100 as a final step on all currency output values, this rounds to the nearest cent before display. For summing bracket tax values, the accumulation uses raw floats (where the error is negligible per bracket) and only rounds at the final output stage.

Keeping Tax Bracket Data Accurate. Tax brackets change annually. Encoding them as static JavaScript data means the application will become stale after each tax year. The data structure was designed to make updates straightforward: each jurisdiction is a named key in the brackets object, and each bracket is a simple { min, max, rate } object that mirrors the CRA table format directly. Adding or updating a province requires only editing the data object, not touching any calculation logic. A // DATA VERSION: 2023 comment is placed prominently at the top of the data file to make staleness immediately visible.

What I Learned

  • How Canadian marginal tax brackets work in practice: income is taxed at each bracket's rate only on the portion that falls within that bracket, not on the entire income
  • Effective tax rate (total tax divided by gross income) versus marginal rate (the rate applied to the last dollar earned), a common source of public confusion that good UI can clarify
  • IEEE 754 floating-point precision errors are real and unavoidable in JavaScript currency arithmetic; the correct mitigation is Math.round(v * 100) / 100 at output time, not during accumulation
  • Input debouncing with setTimeout/clearTimeout prevents expensive operations from running on every keystroke and is the standard pattern for real-time input-driven computation
  • Encoding structured data (tax brackets) as clean JavaScript objects rather than inline HTML makes the data easy to update, test, and eventually replace with an API call
  • Separating calculation logic from rendering logic (pure calcSummary() function vs renderResults() function) makes both independently testable and reusable

How I Built It

Phase 1: Researching and encoding the bracket data. I pulled the 2023 federal brackets from CRA publications and the provincial tables, then encoded each jurisdiction as a named array of { min, max, rate } objects. Top brackets use Infinity as the upper bound so the same loop works for all of them.

Phase 2: The marginal tax engine. The core calcTax(income, brackets) function walks each bracket, taxes only the income slice that falls inside it, and records a per-bracket breakdown alongside the total. Because calculation is separate from rendering, the same pure function drives both the summary card and the detail tables.

Phase 3: Totals, rates, and correct rounding. Federal and provincial totals are combined, then effective rate and net income are derived. Every currency value passes through Math.round(v * 100) / 100 at output time, which is what keeps binary floating-point representation errors from leaking cent artefacts into the results.

Phase 4: The form and data-driven dropdown. The province dropdown is populated from the keys of the bracket data object, so adding a jurisdiction is a data edit, not an HTML edit. The income input is a type="number" field with HTML5 validation.

Phase 5: Real-time rendering with debouncing. Both inputs listen for input events and re-render results immediately, with a 150 ms debounce so rapid typing does not thrash the DOM. The results card shows headline figures plus collapsible per-bracket breakdown tables, with the effective rate colour-coded by band.

What You Need

CategoryItemNotes
ToolA modern web browserThe entire app runs client-side; no server is required
ToolA local dev server (e.g. npx serve)Optional for development; any static host works for deployment
SoftwareCode editorUsed for the bracket data file and the calculation engine
SoftwareCRA tax publications for the target yearThe only data source needed; bracket data is the app's raw material
SkillJavaScript fundamentalsArray iteration, object data structures, event listeners
SkillHow marginal tax brackets workEach rate applies only to income inside that bracket, not to the whole income
SkillFloating-point awarenessKnowing that 0.1 + 0.2 !== 0.3 in IEEE 754 and rounding at output
SkillHTML/CSSForm inputs, responsive results card, colour-coded rate display

Challenges and Lessons Learned

Floating-point precision in money math. JavaScript's IEEE 754 arithmetic makes naive currency accumulation produce values like $15,234.999999. Rounding once at output with Math.round(v * 100) / 100 was the right fix; rounding during accumulation would have introduced a different, subtler bias. The lesson: in currency code, know exactly where your rounding boundaries are.

Data staleness by design. Hard-coding the 2023 brackets means the app needs an annual data update. I deliberately made the data file a clean mirror of the CRA table format with a prominent // DATA VERSION: 2023 marker, so the update is mechanical. The honest takeaway: any app built on regulated data needs a refresh plan, not just a refresh.

Separation of calculation and rendering. Keeping calcSummary() pure (inputs in, numbers out) meant the same logic could be tested, reused, and reasoned about independently of the DOM. That one habit made every later change cheaper.

What Is Next

  • Adding the remaining provinces and territories so all Canadian jurisdictions are covered
  • A data-file versioning scheme with a visible "tax year" indicator and staleness warning
  • Comparison mode: side-by-side results for two provinces at the same income
  • RRSP and deduction inputs to show tax savings from common deductions
  • Client-side unit tests for the bracket engine, including boundary incomes at exact bracket edges
  • An export option (CSV/PDF) for the full per-bracket breakdown
JavaScript HTML5 CSS3 Tax Calculator Financial App Vanilla JS