Thanks for helping make this more accurate and more useful! This is a small, friendly project — PRs of all sizes are welcome.
The site is fully static: plain HTML, CSS, and JS loaded as classic
<script> tags. There is:
- No
npm install— there are no dependencies. - No build step, no bundler, no ES modules — deliberately, so the page keeps
working when opened directly from
file://.
To test a change, just open index.html in your browser and confirm the app
still works. That's the whole workflow. Please don't introduce a build step or
convert the scripts to ES modules — it would break file:// usage.
If you edited a data file (data/states.js, data/changelog.js,
data/vehicle-*.js), also open tests/selftest.html — it's a zero-dependency
page that checks the data for the most common mistakes (a malformed state row, a
bad changelog date, an interior with no cabin photo, etc.) and shows a green
"all checks passed" banner or a red list of what's wrong. No install needed.
This is the highest-value contribution. Each state is a row of estimated defaults. The schema:
| Field | Meaning |
|---|---|
tax |
Upfront purchase/sales tax rate, as a percentage of price |
title |
One-time title fee (flat $) |
reg |
Registration fee (flat $) |
evFee |
Annual EV surcharge — the extra yearly fee many states charge EVs |
propTax |
Annual vehicle property-tax rate, per $100 of value (use 0 if your state has none) |
ins |
Average annual insurance premium ($) |
kwh |
Average residential electricity rate (¢/kWh, EIA Table 5.6.A) |
gas |
Average regular gasoline price ($/gal, AAA state average) |
verified |
'YYYY-MM' the row was last checked against sources |
Guidelines:
- Cite a source in your PR. Link the relevant state DMV, department of motor vehicles, or department of revenue page you used. Data changes without a source are hard to accept.
- Bump
verifiedto the current'YYYY-MM'whenever you change any value in a row. - If your state has no vehicle property tax, set
propTaxto0(not blank). - Keep values as plain numbers — no
$,%, or commas.
The validation/ directory holds a repeatable audit playbook: open
validation/export.html to generate a copy-paste prompt per state, run it
through any LLM with web search, and it returns a structured JSON verdict per
field. validation/README.md describes the review-and-merge loop. This is how
the verified stamps stay honest — audits run semiannually (January and July).
Each vehicle lives in its own self-contained file — data/vehicle-r2.js,
data/vehicle-r1s.js, and so on. A file registers itself onto a shared map,
guarded so load order never matters:
var VEHICLES = VEHICLES || {};
VEHICLES.r2 = { name: 'R2', /* … */ };Add the new file as a classic <script> tag in two places: index.html
(before app.js) and tests/selftest.html (which loads the data files with
its own script tags — miss it and the self-test silently never sees your
vehicle). No build step, no ES modules — same rules as everything else here.
VEHICLES.<id> is one object. The top-level keys:
| Key | What's in it |
|---|---|
name |
Short display name, e.g. 'R2' — also the header/toggle label |
trims |
Map of trim id → trim object (name, short, price, drive, range, avail, folder, colors, wheels, interior, includes, optional drives, …). folder is the trim's visualizer path segment |
colors |
Map of color id → {name, price, code, hex, note?, avail?}; code is Rivian's EXP-* retail code used in image URLs |
wheelSwatch |
Map of wheel code → Cloudinary swatch path |
interiors |
Map of interior id → {name, code, price, hex} |
cabins |
Map of interior code → cabin-photo path |
addons |
Array of {id, name, price, grp, launchInc?, cmp?, link} — shown in the compare matrix when launchInc/cmp is set |
connectPlus |
Subscription object with plans.yearly / plans.monthly |
accessories |
Grouped gear catalog [{grp, items:[…]}]; gearImg is the image base URL, accFootnote the footnote |
img.program |
Rivian visualizer program segment used in image URLs (R2 = 'gold-iris'; discover R1S's from its live 360 image URL) |
compareSpecs |
Feature rows `[{label, values:{: true |
baseLabel / baseIncludes |
Heading (e.g. 'Included on every R2') + bullet list of what every trim includes |
flagshipTrim |
Trim id to highlight as the halo column (optional; defaults to the last trim) |
verdictNotes |
Optional per-trim copy for the verdict cards under the compare matrix; may embed {tokens} (drive, motors, driveSub, range, hp, z60, tow, avail) filled from that column's live drive + wheel pick. verdictNotesLaunchOff overrides a trim's note while the promo is toggled off. Omit a trim → the app writes a data-driven line |
| optional | draft, owner, verified: 'YYYY-MM' |
id ↔ code cross-reference — the one gotcha. Trims reference colors and
interior by id, but wheelSwatch and cabins are keyed by code
(the EXP-* / interior codes Rivian uses in image URLs). Keep both sides
consistent — a missing swatch or cabin photo is almost always an id-vs-code
mismatch.
The R2/R1S toggle in the header is data-gated: it only appears when two or more non-draft vehicles are loaded. With a single live vehicle the app looks exactly like it does today — no toggle at all. So the multi-vehicle plumbing ships invisibly, and adding a live vehicle file is what turns the toggle on.
While a vehicle is a work in progress, commit it with draft: true in the
file header. A draft vehicle:
- stays out of the toggle (it doesn't count toward the ≥2), yet
- is still exercised by
tests/selftest.html, so you can validate it before launch.
To QA a draft in the real UI: draft vehicles show automatically in local
dev — opening index.html from file://, or on localhost — each tagged with
a small draft badge so they can't be mistaken for shipped data. On a deployed
preview (e.g. a Vercel preview URL), add ?preview=1 to reveal them.
Production links have neither, so drafts never show there. (?preview=0 forces the
production, R2-only view even locally, if you want to check it.) That's how "draft"
and "production" stay distinct — no build step, no hardcoded domain.
Drop the flag once the data is complete and green. The rule of thumb: we never show a toggle that leads nowhere.
- Prices — from the builder on rivian.com and, for gear, the Gear Shop's Shopify product JSON.
- Specs — from the builder (range, power, 0–60, tow, etc.).
- Images — discover the visualizer CDN program/path from the live configurator's 360 image URL, then hotlink it. Never copy or redistribute Rivian's images; reference them at display time, exactly as the R2 does.
- Availability labels — follow the house convention: label "Coming soon" only, never transient stock states.
- Cite your sources in the PR, and stamp each verified area with
verified: 'YYYY-MM', bumping it whenever a value changes.
One nice difference: a shipping vehicle like the R1S has official, stable prices and specs, so its data is higher-confidence than the R2's pre-release estimates.
Same as any change here — but do both:
- Open
index.htmlfromfile://and confirm the app still works (with one live vehicle it should look unchanged). - Open
tests/selftest.html— it loops over every vehicle inVEHICLES, drafts included. Any non-draft vehicle must come up green before it goes live.
When a vehicle ships in the app, or its options or prices change, add a
matching entry in data/changelog.js tagged config: true (see the
changelog section below).
A live vehicle needs a committed data owner — list them in the file
header (owner: '…') — because live data is held to the semiannual re-verify
cadence (January and July), alongside the state data. This is what keeps the
project low-maintenance: every live surface has someone on the hook for it.
No owner yet? Two good options:
- Leave the file
draft: trueuntil one steps up, or - Fork a standalone configurator for your vehicle and periodically resync
the shared bits —
data/states.jsandvalidation/— from upstream.
Either way, we avoid taking on a live vehicle that nobody has agreed to keep accurate.
The vehicle layer is a switcher — one active vehicle at a time. A true
cross-vehicle matrix (an R1S column next to R2 trims in one table) is a
deliberate non-goal for now: it needs a shared feature taxonomy (stable feature
keys with per-vehicle labels — today's compareSpecs labels are free text and
won't line up across vehicles), per-column image programs, and a home for
per-vehicle concepts like baseIncludes and promo rows in a mixed table.
Meanwhile, cross-vehicle cost comparison already works: each saved scenario
on the Cost tab snapshots its source vehicle and names it on the card, and the
scenarios chart overlays R2 and R1S curves together. Revisit the matrix idea
after the R1S dataset is verified and live.
The app shows a version history. Each entry looks like:
{ date: '2026-07-04', changes: ['Short description of what changed'] }Each item in changes is either a plain string, or an object that tags the
bullet as an R2 configuration-option change with config: true, which shows
an inline "Config" pill:
{ date: '2026-07-04', changes: [
{ text: 'Dual-motor AWD is now configurable', config: true },
'Fixed a rounding error in the fee breakdown'
] }List config bullets first within an entry, then the untagged ones.
Leave a bullet as a plain string for routine changes (bug fixes, state-data corrections, copy tweaks, styling, etc.).
Use it when a bullet touches the R2's set of orderable options or their prices — for example:
- A new option becomes configurable, or an "upcoming" option ships, or
- An option's price or availability is corrected in the builder.
It is not for general updates. If you're just fixing a typo or a chart, leave it off.
Some R2 options are shown as upcoming (with a date chip) before you can actually order them. When Rivian makes one of these available, a single PR should do both:
- Update the config data in
data/vehicle-r2.js— mark the option available and remove its "upcoming" date chip. - Add a changelog bullet in
data/changelog.jstaggedconfig: true, describing what became available.
Doing both in one PR keeps the configurator and its changelog in sync.
- Keep PRs focused — one logical change per PR is easiest to review.
- Describe the change and, for any data update, link your source.
- Confirm you opened
index.htmllocally and it still works.
That's it — thanks for contributing!