"Sales tax on a car" sounds like price × rate. Then you read the statutes.
I built a calculator for the total cost of buying a car — sales tax, title, registration and plates — for five US states. Reading the statutes first made it clear that one formula with a few parameters per state wouldn't hold up: the states differ in what they tax, which local rate applies and how fees are shaped. So the design is one small pure function per state, parameters kept in JSON next to the source they came from, and tests written from hand-worked examples.
Here are the rules that drove that design.
The rules that look alike but aren't
1. What gets taxed. Most states tax the price minus your trade-in. California doesn't: the trade-in allowance stays in the taxable amount (CDTFA Regulation 1654), and the dealer's document fee is taxed too. Texas taxes private-party sales on the higher of the price or 80% of a "standard presumptive value", so writing a low price on the bill of sale doesn't lower the tax.
2. Which local rate. California, Florida, New York and Pennsylvania all add a local rate — and in every case it's the rate of the address where the car will be registered, not where the dealer is. Buying in a cheaper county does nothing. California has 541 distinct city/area rates; New York has 76 jurisdictions.
3. A cap hidden in the local rate. Florida's county surtax applies only to the first $5,000 of the price. A "6% + 1.5%" county is not 7.5% on a $40,000 car — it's 6% on $40,000 plus 1.5% on $5,000. Showing Florida as "6%–8%" in a rate table is technically true and practically misleading; I ended up labeling it "6% + county surtax on the first $5,000".
4. Fees with three different shapes. Registration is flat in Pennsylvania ($48/year), weight-based in Florida and New York (New York has 55 weight brackets for a two-year term), and value-based in California (0.65% of the car's value plus a tiered fee). Some fees only apply the first time: Florida charges $225 when you register a car without a plate to transfer from one you sold.
5. Flat taxes that replace the percentage. Texas charges a flat $90 "new resident tax" instead of use tax when you move in with your own car, $5 on an even exchange, $10 on a gift from family.
One formula with switches for all of this becomes unreadable fast. One function per state stays readable.
The shape
Every state function has the same signature and returns line items, so the UI doesn't care which state it's rendering:
export interface Input { price: number; tradeIn?: number; docFee?: number; purchase?: "new" | "used" | "private"; place?: string; // county / city key for local rates weight?: number; // lbs, for weight-based registration plateTransfer?: boolean; newResident?: boolean; spv?: number; // Texas standard presumptive value } export interface Line { key: string; label: string; amount: number } export function florida(i: Input, d: FloridaData): Result { const st = d.salesTax, reg = d.registration; const surtax = (i.place && st.surtax[i.place]) || 0; const lines: Line[] = []; const dealer = i.purchase !== "private"; const taxable = Math.max(0, i.price - (dealer ? i.tradeIn ?? 0 : 0)) + (dealer ? i.docFee ?? 0 : 0); lines.push({ key: "tax", label: "State sales tax (6%)", amount: taxable * st.rate }); if (surtax) lines.push({ key: "taxlocal", label: "County surtax", amount: Math.min(taxable, st.surtaxCap) * surtax }); lines.push({ key: "title", label: "Certificate of title", amount: d.title.fee }); lines.push({ key: "reg", label: "Registration", amount: byWeight(reg.byWeight, i.weight ?? 3500) }); if (!i.plateTransfer) { lines.push({ key: "initial", label: "Initial registration fee", amount: reg.initialRegistration }); lines.push({ key: "plate", label: "New license plate", amount: reg.newPlate }); } return finish(i, taxable, lines); } The numbers live in a JSON file per state, each with an asOf date and the list of sources it came from. When a legislature changes a fee, the diff is one number in one file, and the source URL sits right next to it.
Tests from hand-worked examples
Each test is a purchase I worked out on paper from the statute, then asserted line by line:
test("florida: surtax only on the first $5,000", () => { const r = florida({ price: 30_000, tradeIn: 10_000, docFee: 999, purchase: "new", place: "Orange", weight: 3_400 }, FL); near(r.taxable, 20_999); near(line(r, "tax"), 1259.94); // 6% of 20,999 near(line(r, "taxlocal"), 25); // 0.5% of 5,000 — not of 20,999 }); test("california: no trade-in credit, doc fee taxable", () => { const r = california({ price: 30_000, tradeIn: 10_000, docFee: 85, purchase: "new", place: "Los Angeles" }, CA); near(r.taxable, 30_085); // trade-in ignored, doc fee added }); Node's built-in test runner runs TypeScript directly with --experimental-strip-types, so there's no build step for the tests at all. Eleven tests cover all five states, including the edge cases: the Florida cap, California's missing trade-in credit, Texas's presumptive-value floor for private sales and its flat new-resident tax.
Data traps
- Secondary sources disagree with the law. Several fee sites say New York's title fee dropped to $5 in April 2026. The statute (Vehicle and Traffic Law §2125) has two versions: $50 until April 1, 2028, then $5. Reading the primary source settled it.
- Rate tables expire. Florida publishes each county surtax with an end date. Four counties have levies ending December 31, 2026, so a "current rate" is only current until then. I kept the start/end dates in the data instead of just the total.
- "Emissions county" isn't the same list everywhere. Texas charges a $33 title fee in nonattainment/affected counties and $28 elsewhere. I had listed a county from memory that isn't on the motor vehicle department's emissions list — another reason to keep the list in data, with its source.
- Leave out what you can't confirm. A few states cap dealer documentation fees, but I couldn't confirm every current cap from an official source. Those caps are mentioned in notes and left out of the totals rather than guessed.
Takeaways
- When rules differ in shape, not just in parameters, separate functions beat one configurable formula.
- Keep every number next to its source and the date you checked it.
- Write tests from worked examples of the actual rule — especially the edge cases (caps, credits that don't apply, flat taxes that replace a percentage).
The calculator and the CSV data behind it (California's 541 rates, Florida surtaxes with their end dates, New York rates and registration add-ons) are at carsalestax.com.
![Why Object.keys(user) Returns string[], Not (keyof User)[]: TypeScript Means It](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2q4w48b9bzs43tynyhsf.png)
