<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[SalaryPayTools Blog]]></title><description><![CDATA[SalaryPayTools Blog]]></description><link>https://salarypaytools.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 02:54:52 GMT</lastBuildDate><atom:link href="https://salarypaytools.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Fast Salary and Paycheck Calculator with React, Vite, and TypeScript]]></title><description><![CDATA[I recently shipped SalaryPayTools, a small set of free US pay calculators: hourly to salary, salary to hourly, overtime, and a take-home paycheck estimate…]]></description><link>https://salarypaytools.hashnode.dev/building-a-fast-salary-and-paycheck-calculator-with-react-vite-and-typescript</link><guid isPermaLink="true">https://salarypaytools.hashnode.dev/building-a-fast-salary-and-paycheck-calculator-with-react-vite-and-typescript</guid><category><![CDATA[React]]></category><category><![CDATA[vite]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Faishal]]></dc:creator><pubDate>Tue, 27 Oct 2026 13:06:51 GMT</pubDate><content:encoded><![CDATA[<p>I recently shipped <a href="https://salarypaytools.com">SalaryPayTools</a>, a small set of free US pay calculators: hourly to salary, salary to hourly, overtime, and a take-home paycheck estimate for a few states. The tools are simple. Building them well turned out to be more interesting than I expected, mostly because money math punishes sloppy code and search engines punish sloppy single-page apps.</p>
<p>This post covers what I learned: how I structure the calculation logic in TypeScript, how I think about calculator state, why I designed for phones first, and the SEO lessons I'd tell anyone shipping a React + Vite site that needs to be found.</p>
<p><strong>Live demo:</strong> <a href="https://salarypaytools.com">salarypaytools.com</a></p>
<h2>Why React + Vite + TypeScript</h2>
<p>A calculator site is mostly forms and instant feedback. React fits that well, and Vite keeps the dev loop fast: the dev server starts in a moment and hot reload doesn't get in the way when you are tweaking inputs fifty times an hour.</p>
<p>TypeScript was the most important choice. Pay math has many values that are all just <code>number</code>: hourly rates, hours per week, weeks per year, annual totals, percentages. Mixing them up is easy. Named types and small pure functions make those mistakes much harder to write.</p>
<h2>Keep the math pure and boring</h2>
<p>My rule is that the math lives in plain functions with no React in them. Components gather input and display output. The functions do the arithmetic, and I can test them in isolation.</p>
<p>Here is the core of hourly to annual conversion:</p>
<pre><code class="language-ts">export interface HourlyInput {
  hourlyRate: number;     // dollars per hour
  hoursPerWeek: number;   // regular hours, e.g. 40
  weeksPerYear: number;   // paid weeks, e.g. 52
  overtimeHoursPerWeek?: number;
  overtimeMultiplier?: number; // 1.5 is the common case
}

export interface PayBreakdown {
  annual: number;
  monthly: number;
  biweekly: number;
  weekly: number;
}

const round2 = (n: number) =&gt; Math.round(n * 100) / 100;

export function hourlyToAnnual(input: HourlyInput): PayBreakdown {
  const {
    hourlyRate,
    hoursPerWeek,
    weeksPerYear,
    overtimeHoursPerWeek = 0,
    overtimeMultiplier = 1.5,
  } = input;

  const weekly =
    hourlyRate * hoursPerWeek +
    hourlyRate * overtimeMultiplier * overtimeHoursPerWeek;

  const annual = weekly * weeksPerYear;

  return {
    annual: round2(annual),
    monthly: round2(annual / 12),
    biweekly: round2(annual / 26),
    weekly: round2(weekly),
  };
}
</code></pre>
<p>With the usual full-time assumptions (40 hours a week, 52 weeks), $25 an hour comes out to $1,000 a week and $52,000 a year. That 2,080-hour year is a convention, not a law of nature, so I keep weeks per year as an input. Someone with two unpaid weeks off should be able to say so.</p>
<p>Two habits that saved me bugs:</p>
<ol>
<li><strong>Derive everything from one source.</strong> Monthly and biweekly come from the annual figure. They are never calculated separately, so they can't drift apart.</li>
<li><strong>Round only at the edges.</strong> Keep full precision through the math and round for display. Rounding in the middle adds up to visible cent errors.</li>
</ol>
<h2>A simplified FICA calculation</h2>
<p>The take-home side starts with payroll taxes. For an employee, FICA is two parts:</p>
<ul>
<li><strong>Social Security:</strong> 6.2% of wages, up to an annual wage cap (the Social Security wage base). The cap changes every year. It is $176,100 for 2025 and $184,500 for 2026.</li>
<li><strong>Medicare:</strong> 1.45% of all wages, with no cap.</li>
</ul>
<pre><code class="language-ts">const SOCIAL_SECURITY_RATE = 0.062;
const MEDICARE_RATE = 0.0145;

// Update every year from the SSA's published wage base.
const SOCIAL_SECURITY_WAGE_BASE: Record&lt;number, number&gt; = {
  2025: 176_100,
  2026: 184_500,
};

export function employeeFica(annualWages: number, year: number) {
  const wageBase = SOCIAL_SECURITY_WAGE_BASE[year];
  if (wageBase === undefined) {
    throw new Error(`No Social Security wage base configured for ${year}`);
  }

  const socialSecurity =
    Math.min(annualWages, wageBase) * SOCIAL_SECURITY_RATE;
  const medicare = annualWages * MEDICARE_RATE;

  return {
    socialSecurity: round2(socialSecurity),
    medicare: round2(medicare),
    total: round2(socialSecurity + medicare),
  };
}
</code></pre>
<p>For $52,000 in wages that is $3,224 of Social Security plus $754 of Medicare, $3,978 in total, which is the familiar 7.65%. Above the wage base, the effective rate drops because Social Security stops.</p>
<p><strong>The caveat.</strong> This is simplified on purpose. It leaves out the <strong>Additional Medicare Tax</strong>: an extra 0.9% that employers must withhold on wages above $200,000 in a calendar year. The final liability depends on filing status, so withholding and what you actually owe can differ. It also assumes a single job. With two employers, each one withholds Social Security up to the cap on its own, and the excess comes back when you file. And it ignores pre-tax deductions, such as some retirement and health contributions, that can change which wages are taxed.</p>
<p>I throw an error for an unknown year instead of quietly reusing an old cap. A wrong number that looks right is the worst result a pay calculator can give. In the UI, take-home results are labelled as estimates, and they currently cover only a few states (Texas, Arkansas, Illinois, and North Carolina) because each state's rules need their own care.</p>
<h2>State that survives a refresh</h2>
<p>Losing your numbers on refresh, or when you switch tabs on a phone, feels broken. There are a few layers to think about. I'm describing the pattern here, not claiming every page of my site does all of it today:</p>
<ul>
<li><strong>URL query params</strong> for the main inputs (<code>?rate=25&amp;hours=40</code>). Results become shareable and bookmarkable, and the back button works.</li>
<li><strong>sessionStorage</strong> for in-progress values that don't belong in a URL. They survive a reload in the same tab and are gone when the tab closes, which suits a privacy-minded tool.</li>
<li><strong>localStorage</strong> only for real preferences you'd want next week.</li>
</ul>
<p>A minimal hook for the URL layer:</p>
<pre><code class="language-ts">import { useCallback, useState } from "react";

export function useQueryNumber(key: string, fallback: number) {
  const read = () =&gt; {
    const raw = new URLSearchParams(window.location.search).get(key);
    const n = raw === null ? NaN : Number(raw);
    return Number.isFinite(n) ? n : fallback;
  };

  const [value, setValue] = useState&lt;number&gt;(read);

  const update = useCallback(
    (next: number) =&gt; {
      setValue(next);
      const params = new URLSearchParams(window.location.search);
      params.set(key, String(next));
      window.history.replaceState(null, "", `?${params}`);
    },
    [key]
  );

  return [value, update] as const;
}
</code></pre>
<p><code>replaceState</code> instead of <code>pushState</code> matters. Otherwise every keystroke adds a history entry, and the back button becomes useless.</p>
<h2>Mobile-first, because that's where people are</h2>
<p>Many people check a pay number on a phone, often while looking at a job listing. So I designed for small screens first:</p>
<ul>
<li><code>inputMode="decimal"</code> on number fields, so phones show the numeric keypad.</li>
<li>Big tap targets and labels above the inputs, not tiny placeholders that disappear.</li>
<li>Results update as you type, so there is no "Calculate" button to hunt for.</li>
<li>The key result goes at the top in large type, with the breakdown below it.</li>
</ul>
<p>A plain CSS grid that becomes multi-column at wider widths did more for usability than any component library would have.</p>
<h2>SEO lessons from a client-rendered SPA</h2>
<p>This is where I learned the most. A default Vite React build ships an almost empty <code>index.html</code> and renders everything in JavaScript. Crawlers can run JavaScript, but you are relying on that going well, and social preview scrapers often don't run it at all. What helped:</p>
<ol>
<li><strong>Prerender every route.</strong> Each calculator page is generated as real HTML at build time and then hydrated. Today each route on the site serves its own static HTML with its own <code>&lt;title&gt;</code>, so a crawler sees real content on the first request.</li>
<li><strong>Unique tags per page.</strong> Each route gets its own title, meta description, canonical URL, and Open Graph tags. Twenty pages that all say "Salary Calculator" compete with each other.</li>
<li><strong>A sitemap and robots.txt.</strong> The site has an XML sitemap listing every calculator and guide, referenced from <code>robots.txt</code> and submitted in Search Console. It is boring, and it helps.</li>
<li><strong>Consistent URLs.</strong> I use trailing slashes everywhere, in links, canonicals, and the sitemap, so the same page doesn't show up under two addresses.</li>
<li><strong>Content, not just widgets.</strong> A calculator alone is thin. A short explanation of the assumptions under each tool helps users and gives search engines something to understand.</li>
</ol>
<h2>What's next</h2>
<p>More state coverage for take-home pay, tests that pin the tax tables to each year's published figures, and more of the state patterns above on every page. If you're building something similar, my one piece of advice: keep the math pure, put the yearly constants where you can't forget them, and prerender from day one.</p>
<p>You can try the calculators at <a href="https://salarypaytools.com">salarypaytools.com</a>. I'd love feedback, especially on the math, from anyone who has built payroll tools.</p>
]]></content:encoded></item><item><title><![CDATA[Salary vs Hourly: How to Compare Two Job Offers (with Examples)]]></title><description><![CDATA[By Faishal, maker of SalaryPayTools You have two offers. One says "$62,000 a year." The other says "$28 an hour." Which pays more? The quick trick is to…]]></description><link>https://salarypaytools.hashnode.dev/salary-vs-hourly-how-to-compare-two-job-offers-with-examples</link><guid isPermaLink="true">https://salarypaytools.hashnode.dev/salary-vs-hourly-how-to-compare-two-job-offers-with-examples</guid><category><![CDATA[Career]]></category><category><![CDATA[personal finance]]></category><category><![CDATA[salary]]></category><category><![CDATA[jobs]]></category><dc:creator><![CDATA[Faishal]]></dc:creator><pubDate>Tue, 27 Oct 2026 12:38:25 GMT</pubDate><content:encoded><![CDATA[<p><em>By Faishal, maker of SalaryPayTools</em></p>
<p>You have two offers. One says "$62,000 a year." The other says "$28 an hour." Which pays more?</p>
<p>The quick trick is to multiply the hourly rate by 2,080 (40 hours × 52 weeks). That turns $28 an hour into $58,240, so the salary looks like the winner. But that number leaves out most of what decides how much a job is really worth: how many hours you'll actually work, whether you get paid for the extra ones, how much paid time off you get, and what the benefits cost you.</p>
<p>This guide walks through each of those, then compares two concrete offers with the math shown.</p>
<h2>Start with classification: exempt vs nonexempt</h2>
<p>In the US, the federal Fair Labor Standards Act (FLSA) splits most employees into two groups:</p>
<ul>
<li><strong>Nonexempt</strong> employees must be paid overtime, at least 1.5 times their regular rate, for every hour worked over 40 in a workweek.</li>
<li><strong>Exempt</strong> employees are not owed overtime under federal law. To be exempt, a job generally has to pass a salary test <em>and</em> a duties test (executive, administrative, professional and a few other categories).</li>
</ul>
<p>Two common myths are worth clearing up:</p>
<ol>
<li><strong>"Salaried" does not automatically mean "exempt."</strong> Some salaried employees are nonexempt and still earn overtime.</li>
<li><strong>"Hourly" almost always means nonexempt.</strong> If you're paid by the hour, extra hours usually mean extra pay.</li>
</ol>
<p>Some states add their own rules on top of federal law. California, for example, has daily overtime. So check your state's rules too. If an offer letter doesn't say whether the role is exempt, ask. It changes how you should value every extra hour.</p>
<h2>The five things to compare</h2>
<h3>1. Hours you'll actually work</h3>
<p>A salary is a fixed amount for however many hours the job takes. If the role routinely runs 45 or 50 hours a week, your real hourly rate drops. Ask the hiring manager, or better, someone already on the team, what a normal week looks like and how often "crunch" happens.</p>
<h3>2. Overtime</h3>
<p>For a nonexempt hourly job, overtime can add real money. But it's rarely guaranteed. Count on overtime only if it's steady and predictable. Then run the numbers both with and without it.</p>
<h3>3. Unpaid hours</h3>
<p>These are the hours that quietly shrink your pay:</p>
<ul>
<li><strong>Salaried exempt:</strong> evening emails, weekend deploys, on-call weeks. None of it adds a dollar.</li>
<li><strong>Hourly:</strong> an unpaid 30-minute lunch means you're at work 8.5 hours for 8 hours of pay. Under the FLSA, nonexempt employees must be paid for all time worked, including work done "off the clock." Still, ask how the company handles things like shift handovers and required training.</li>
</ul>
<h3>4. Paid time off and holidays</h3>
<p>PTO is pay for hours you don't work. Two weeks off versus five is a big difference in your effective hourly rate, even when the yearly pay is the same. Ask how many PTO days and paid holidays you get, and whether sick days are counted separately.</p>
<h3>5. Benefits</h3>
<p>Health insurance premiums, retirement matching, and other perks can swing an offer by thousands of dollars. Look at what <em>you</em> pay each month for health coverage and how much the employer adds to your retirement account (and what you must put in to get it).</p>
<h2>A worked example: Offer A vs Offer B</h2>
<p>Here are two made-up but realistic offers. All numbers are for illustration only.</p>
<p><strong>Offer A: Salaried, exempt</strong></p>
<ul>
<li>$62,000 per year</li>
<li>The team says a typical week is about 45 hours</li>
<li>15 PTO days + 10 paid holidays (25 days = 5 weeks off)</li>
<li>401(k) match of 4% of salary, if you contribute at least 4%</li>
<li>Your health premium: $150 per month</li>
</ul>
<p><strong>Offer B: Hourly, nonexempt</strong></p>
<ul>
<li>$28 per hour, 40-hour schedule</li>
<li>About 150 overtime hours a year (roughly 3 a week), based on what current staff say</li>
<li>10 PTO days + 5 paid holidays (15 days = 3 weeks off), paid at 8 hours a day</li>
<li>No retirement match</li>
<li>Your health premium: $300 per month</li>
</ul>
<h3>Step 1: Gross pay</h3>
<p><strong>Offer A:</strong> $62,000.</p>
<p><strong>Offer B:</strong></p>
<ul>
<li>Base pay: $28 × 2,080 hours = <strong>$58,240</strong> (this already includes the paid PTO and holiday hours)</li>
<li>Overtime rate: $28 × 1.5 = $42 per hour</li>
<li>Overtime pay: 150 × $42 = <strong>$6,300</strong></li>
<li>Total: $58,240 + $6,300 = <strong>$64,540</strong></li>
</ul>
<p>On gross pay alone, Offer B is ahead by $2,540.</p>
<h3>Step 2: Hours actually worked</h3>
<p><strong>Offer A:</strong> 52 weeks − 5 weeks off = 47 working weeks. 47 × 45 hours = <strong>2,115 hours</strong>.</p>
<p><strong>Offer B:</strong> 2,080 paid hours − 120 PTO and holiday hours (15 days × 8) = 1,960 regular hours worked. Add 150 overtime hours for a total of <strong>2,110 hours</strong>.</p>
<p>Both jobs ask for almost the same time each year.</p>
<h3>Step 3: Effective hourly rate (pay only)</h3>
<ul>
<li>Offer A: $62,000 ÷ 2,115 = <strong>$29.31 per hour</strong></li>
<li>Offer B: $64,540 ÷ 2,110 = <strong>$30.59 per hour</strong></li>
</ul>
<p>Offer B still leads.</p>
<h3>Step 4: Add benefits</h3>
<p><strong>Offer A:</strong></p>
<ul>
<li>401(k) match: 4% × $62,000 = +$2,480</li>
<li>Health premiums: $150 × 12 = −$1,800</li>
<li>Total value: $62,000 + $2,480 − $1,800 = <strong>$62,680</strong></li>
</ul>
<p><strong>Offer B:</strong></p>
<ul>
<li>No match</li>
<li>Health premiums: $300 × 12 = −$3,600</li>
<li>Total value: $64,540 − $3,600 = <strong>$60,940</strong></li>
</ul>
<p>Now divide by hours worked:</p>
<ul>
<li>Offer A: $62,680 ÷ 2,115 = <strong>$29.64 per hour</strong></li>
<li>Offer B: $60,940 ÷ 2,110 = <strong>$28.88 per hour</strong></li>
</ul>
<p><strong>The result flips.</strong> Once benefits are counted, Offer A is worth $1,740 more a year, even though its salary looked lower than Offer B's pay with overtime.</p>
<h3>Step 5: Test your assumptions</h3>
<p>The answer depends on two guesses, so stress-test both.</p>
<p><strong>What if Offer A's "45 hours" is really 50?</strong> 47 weeks × 50 hours = 2,350 hours. Then $62,680 ÷ 2,350 = <strong>$26.67 per hour</strong>. Offer A falls well behind.</p>
<p><strong>What if Offer B's overtime dries up?</strong></p>
<ul>
<li>Total value: $58,240 − $3,600 = $54,640</li>
<li>Hours worked: 1,960</li>
<li>$54,640 ÷ 1,960 = <strong>$27.88 per hour</strong></li>
</ul>
<p>So the real question is which risk you'd rather carry. With Offer A, you risk long hours for no extra pay. With Offer B, you risk losing overtime you were counting on. Your answer depends on the team, the manager, and how much your evenings matter to you.</p>
<h2>A simple checklist before you decide</h2>
<ol>
<li>Ask whether the role is <strong>exempt or nonexempt</strong>.</li>
<li>Get a realistic <strong>weekly hours</strong> estimate from someone on the team.</li>
<li>Count <strong>PTO and paid holidays</strong> in hours.</li>
<li>Only count <strong>overtime</strong> you're fairly sure of, and run a no-overtime version too.</li>
<li>Add the employer's <strong>retirement match</strong> (only if you'll contribute enough to get it), and subtract your <strong>premiums</strong>.</li>
<li>Divide total value by <strong>hours actually worked</strong>.</li>
<li>Do it twice: once with your best guess and once with a pessimistic guess.</li>
</ol>
<p>If you'd rather not do the conversions by hand, I built a free <a href="https://salarypaytools.com">hourly to salary calculator on SalaryPayTools</a> that handles the basic salary and hourly math. Then plug in your own PTO, overtime and benefits numbers from the steps above.</p>
<h2>One last note on taxes</h2>
<p>Everything above compares <strong>pre-tax value</strong>. What lands in your bank account depends on federal income tax, Social Security and Medicare, any state and local taxes, and your own choices, like how much you put into a 401(k) or health plan. If two offers are in different states, compare the take-home pay too, not just the gross. For anything close, a tax professional can tell you exactly how your situation works out.</p>
<p>The biggest lesson from the example is simple: <strong>an offer is a rate per hour of your life, not a number on a letter.</strong> Compare the hours, the time off and the benefits, and the "obvious" choice is often the other one.</p>
]]></content:encoded></item></channel></rss>