<div id="rp-wheel-calc">
<style>
#rp-wheel-calc,
#rp-wheel-calc * {
box-sizing: border-box;
}
#rp-wheel-calc {
--bg: #ffffff;
--panel: #f6f7f8;
--line: #d9dde2;
--text: #111317;
--muted: #656d78;
--accent: #c71f2d;
--good: #1c7c48;
--warn: #9b6510;
--old: #8a919a;
--new: #c71f2d;
font-family: Arial, Helvetica, sans-serif;
color: var(--text);
max-width: 1180px;
margin: 20px auto;
}
#rp-wheel-calc .wc-shell {
background: var(--bg);
border: 1px solid var(--line);
border-radius: 16px;
overflow: hidden;
}
#rp-wheel-calc .wc-head {
padding: 24px;
border-bottom: 1px solid var(--line);
}
#rp-wheel-calc h2 {
margin: 0 0 6px;
font-size: 30px;
}
#rp-wheel-calc .wc-sub {
color: var(--muted);
font-size: 14px;
line-height: 1.5;
}
#rp-wheel-calc .wc-grid {
display: grid;
grid-template-columns: 1fr 1fr;
}
#rp-wheel-calc .wc-card {
padding: 22px 24px;
border-bottom: 1px solid var(--line);
}
#rp-wheel-calc .wc-card + .wc-card {
border-left: 1px solid var(--line);
}
#rp-wheel-calc .wc-card-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 18px;
font-size: 18px;
font-weight: 700;
}
#rp-wheel-calc .wc-dot {
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--old);
}
#rp-wheel-calc .new .wc-dot {
background: var(--new);
}
#rp-wheel-calc .wc-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 13px;
}
#rp-wheel-calc .wc-full {
grid-column: 1 / -1;
}
#rp-wheel-calc label {
display: block;
font-size: 12px;
font-weight: 700;
margin-bottom: 6px;
color: #3f4650;
}
#rp-wheel-calc .wc-input-wrap {
position: relative;
}
#rp-wheel-calc input,
#rp-wheel-calc select {
width: 100%;
height: 43px;
border: 1px solid #cfd4da;
border-radius: 8px;
background: white;
padding: 0 42px 0 11px;
font-size: 15px;
}
#rp-wheel-calc input:focus,
#rp-wheel-calc select:focus {
outline: none;
border-color: var(--accent);
}
#rp-wheel-calc .wc-unit {
position: absolute;
right: 11px;
top: 50%;
transform: translateY(-50%);
color: var(--muted);
font-size: 12px;
pointer-events: none;
}
#rp-wheel-calc .wc-offset-row {
display: grid;
grid-template-columns: 125px 1fr;
gap: 8px;
}
#rp-wheel-calc .wc-mini {
margin-top: 6px;
color: var(--muted);
font-size: 11px;
}
#rp-wheel-calc .wc-visual {
padding: 22px 24px;
border-bottom: 1px solid var(--line);
}
#rp-wheel-calc .wc-section-title {
font-size: 17px;
font-weight: 700;
margin-bottom: 4px;
}
#rp-wheel-calc .wc-section-sub {
font-size: 12px;
color: var(--muted);
margin-bottom: 14px;
}
#rp-wheel-calc .wc-svg-wrap {
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel);
overflow: hidden;
}
#rp-wheel-calc svg {
display: block;
width: 100%;
height: auto;
}
#rp-wheel-calc .wc-results {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 20px 24px;
}
#rp-wheel-calc .wc-result {
border: 1px solid var(--line);
border-radius: 10px;
padding: 14px;
}
#rp-wheel-calc .wc-result .k {
font-size: 11px;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
margin-bottom: 7px;
}
#rp-wheel-calc .wc-result .v {
font-size: 20px;
font-weight: 800;
}
#rp-wheel-calc .wc-result .s {
margin-top: 6px;
font-size: 11px;
color: var(--muted);
}
#rp-wheel-calc .wc-summary {
margin: 0 24px 22px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel);
font-size: 14px;
line-height: 1.6;
}
#rp-wheel-calc .wc-note {
padding: 0 24px 24px;
color: var(--muted);
font-size: 11px;
line-height: 1.5;
}
@media (max-width: 800px) {
#rp-wheel-calc .wc-grid {
grid-template-columns: 1fr;
}
#rp-wheel-calc .wc-card + .wc-card {
border-left: 0;
border-top: 1px solid var(--line);
}
#rp-wheel-calc .wc-results {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 500px) {
#rp-wheel-calc .wc-form-grid {
grid-template-columns: 1fr 1fr;
}
#rp-wheel-calc .wc-results {
padding: 16px;
gap: 8px;
}
#rp-wheel-calc .wc-card,
#rp-wheel-calc .wc-head,
#rp-wheel-calc .wc-visual {
padding-left: 16px;
padding-right: 16px;
}
#rp-wheel-calc .wc-summary {
margin-left: 16px;
margin-right: 16px;
}
#rp-wheel-calc .wc-note {
padding-left: 16px;
padding-right: 16px;
}
}
</style>
<div class="wc-shell">
<div class="wc-head">
<h2>Wheel & Tyre Fitment Calculator</h2>
<div class="wc-sub">
Compare wheel width, offset, backspacing, spacers and tyre size.
See how the new setup moves inward and outward relative to the hub.
</div>
</div>
<div class="wc-grid">
<!-- CURRENT SETUP -->
<div class="wc-card">
<div class="wc-card-title">
<span class="wc-dot"></span>
Current setup
</div>
<div class="wc-form-grid">
<div>
<label>Wheel width</label>
<div class="wc-input-wrap">
<input id="oldWheelWidth" type="number" step="0.5" value="8">
<span class="wc-unit">in</span>
</div>
</div>
<div>
<label>Wheel diameter</label>
<div class="wc-input-wrap">
<input id="oldWheelDia" type="number" step="1" value="17">
<span class="wc-unit">in</span>
</div>
</div>
<div class="wc-full">
<label>Wheel position</label>
<div class="wc-offset-row">
<select id="oldMode">
<option value="et">Offset (ET)</option>
<option value="bs">Backspacing</option>
</select>
<div class="wc-input-wrap">
<input id="oldPosition" type="number" step="0.1" value="20">
<span id="oldPositionUnit" class="wc-unit">mm</span>
</div>
</div>
<div id="oldDerived" class="wc-mini"></div>
</div>
<div>
<label>Spacer</label>
<div class="wc-input-wrap">
<input id="oldSpacer" type="number" step="1" value="0">
<span class="wc-unit">mm</span>
</div>
</div>
<div>
<label>Tyre width</label>
<div class="wc-input-wrap">
<input id="oldTyreWidth" type="number" step="5" value="245">
<span class="wc-unit">mm</span>
</div>
</div>
<div>
<label>Tyre profile</label>
<div class="wc-input-wrap">
<input id="oldProfile" type="number" step="5" value="45">
<span class="wc-unit">%</span>
</div>
</div>
</div>
</div>
<!-- NEW SETUP -->
<div class="wc-card new">
<div class="wc-card-title">
<span class="wc-dot"></span>
New setup
</div>
<div class="wc-form-grid">
<div>
<label>Wheel width</label>
<div class="wc-input-wrap">
<input id="newWheelWidth" type="number" step="0.5" value="10">
<span class="wc-unit">in</span>
</div>
</div>
<div>
<label>Wheel diameter</label>
<div class="wc-input-wrap">
<input id="newWheelDia" type="number" step="1" value="17">
<span class="wc-unit">in</span>
</div>
</div>
<div class="wc-full">
<label>Wheel position</label>
<div class="wc-offset-row">
<select id="newMode">
<option value="et">Offset (ET)</option>
<option value="bs">Backspacing</option>
</select>
<div class="wc-input-wrap">
<input id="newPosition" type="number" step="0.1" value="-6">
<span id="newPositionUnit" class="wc-unit">mm</span>
</div>
</div>
<div id="newDerived" class="wc-mini"></div>
</div>
<div>
<label>Spacer</label>
<div class="wc-input-wrap">
<input id="newSpacer" type="number" step="1" value="0">
<span class="wc-unit">mm</span>
</div>
</div>
<div>
<label>Tyre width</label>
<div class="wc-input-wrap">
<input id="newTyreWidth" type="number" step="5" value="275">
<span class="wc-unit">mm</span>
</div>
</div>
<div>
<label>Tyre profile</label>
<div class="wc-input-wrap">
<input id="newProfile" type="number" step="5" value="40">
<span class="wc-unit">%</span>
</div>
</div>
</div>
</div>
</div>
<!-- VISUAL -->
<div class="wc-visual">
<div class="wc-section-title">Wheel position comparison</div>
<div class="wc-section-sub">
The hub mounting face stays fixed. The drawing shows the relative
inboard and outboard position of both setups.
</div>
<div class="wc-svg-wrap">
<svg id="wheelSvg" viewBox="0 0 1000 350">
<rect width="1000" height="350" fill="#f6f7f8"></rect>
<text
x="65"
y="25"
font-size="12"
font-weight="700"
fill="#656d78">
INBOARD / SUSPENSION
</text>
<text
x="935"
y="25"
text-anchor="end"
font-size="12"
font-weight="700"
fill="#656d78">
OUTBOARD / FENDER
</text>
<line
x1="500"
y1="42"
x2="500"
y2="315"
stroke="#111317"
stroke-width="3">
</line>
<text
x="500"
y="38"
text-anchor="middle"
font-size="12"
font-weight="700"
fill="#111317">
HUB FACE
</text>
<g id="oldDrawing"></g>
<g id="newDrawing"></g>
</svg>
</div>
</div>
<!-- RESULTS -->
<div class="wc-results">
<div class="wc-result">
<div class="k">Inner wheel position</div>
<div id="innerWheel" class="v">—</div>
<div class="s">Positive = closer to suspension</div>
</div>
<div class="wc-result">
<div class="k">Outer wheel position</div>
<div id="outerWheel" class="v">—</div>
<div class="s">Positive = more poke</div>
</div>
<div class="wc-result">
<div class="k">Inner tyre position</div>
<div id="innerTyre" class="v">—</div>
<div class="s">Based on nominal tyre width</div>
</div>
<div class="wc-result">
<div class="k">Outer tyre position</div>
<div id="outerTyre" class="v">—</div>
<div class="s">Based on nominal tyre width</div>
</div>
<div class="wc-result">
<div class="k">Effective new ET</div>
<div id="effectiveET" class="v">—</div>
<div class="s">Offset minus spacer</div>
</div>
<div class="wc-result">
<div class="k">New backspacing</div>
<div id="backspacing" class="v">—</div>
<div class="s">Approximate value</div>
</div>
<div class="wc-result">
<div class="k">Tyre diameter change</div>
<div id="diameterChange" class="v">—</div>
<div id="diameterInfo" class="s"></div>
</div>
<div class="wc-result">
<div class="k">At indicated 100 km/h</div>
<div id="speedResult" class="v">—</div>
<div class="s">Estimated actual speed</div>
</div>
</div>
<div id="summary" class="wc-summary"></div>
<div class="wc-note">
<strong>Note:</strong>
Backspacing is estimated using nominal wheel width plus approximately
1 inch for the wheel flanges. Actual wheel flange width can vary by
manufacturer. Actual tyre width also varies between tyre models and
measuring rim widths. Always verify physical clearance before installation.
</div>
</div>
<script>
(function () {
const root = document.getElementById("rp-wheel-calc");
if (!root) return;
const MM_PER_INCH = 25.4;
function el(id) {
return root.querySelector("#" + id);
}
function number(id) {
const value = parseFloat(el(id).value);
return Number.isFinite(value) ? value : 0;
}
/*
Backspacing approximation:
BS = ((nominal wheel width + 1") / 2) + ET / 25.4
ET = (BS - ((nominal wheel width + 1") / 2)) * 25.4
*/
function etToBackspacing(width, et) {
return ((width + 1) / 2) + (et / MM_PER_INCH);
}
function backspacingToEt(width, bs) {
return (bs - ((width + 1) / 2)) * MM_PER_INCH;
}
/*
Store ET internally.
This prevents the ET / Backspacing selector from changing
the actual wheel position when switching input mode.
*/
let oldStoredET = 20;
let newStoredET = -6;
function syncPosition(prefix) {
const width = number(prefix + "WheelWidth");
const mode = el(prefix + "Mode").value;
const input = el(prefix + "Position");
const unit = el(prefix + "PositionUnit");
const storedET =
prefix === "old"
? oldStoredET
: newStoredET;
if (mode === "et") {
input.value = storedET.toFixed(1);
unit.textContent = "mm";
} else {
input.value = etToBackspacing(width, storedET).toFixed(2);
unit.textContent = "in";
}
}
function readPosition(prefix) {
const width = number(prefix + "WheelWidth");
const mode = el(prefix + "Mode").value;
const value = number(prefix + "Position");
let et;
if (mode === "et") {
et = value;
} else {
et = backspacingToEt(width, value);
}
if (prefix === "old") {
oldStoredET = et;
} else {
newStoredET = et;
}
return et;
}
function getSetup(prefix) {
const width = number(prefix + "WheelWidth");
const diameter = number(prefix + "WheelDia");
const spacer = number(prefix + "Spacer");
const tyreWidth = number(prefix + "TyreWidth");
const profile = number(prefix + "Profile");
const et = readPosition(prefix);
/*
A spacer moves the wheel outward,
so it reduces effective positive offset.
*/
const effectiveET = et - spacer;
/*
Approximate physical wheel width:
nominal bead-seat width + 1 inch.
*/
const physicalWheelWidth =
(width + 1) * MM_PER_INCH;
const halfWheel =
physicalWheelWidth / 2;
const innerWheel =
halfWheel + effectiveET;
const outerWheel =
halfWheel - effectiveET;
/*
Tyre comparison uses nominal section width.
*/
const innerTyre =
tyreWidth / 2 + effectiveET;
const outerTyre =
tyreWidth / 2 - effectiveET;
const sidewall =
tyreWidth * profile / 100;
const tyreDiameter =
diameter * MM_PER_INCH +
2 * sidewall;
return {
width,
diameter,
spacer,
tyreWidth,
profile,
et,
effectiveET,
physicalWheelWidth,
innerWheel,
outerWheel,
innerTyre,
outerTyre,
tyreDiameter,
backspacing: etToBackspacing(width, et),
effectiveBackspacing:
etToBackspacing(width, effectiveET)
};
}
function signed(value, decimals, unit) {
if (Math.abs(value) < 0.05) {
value = 0;
}
return (
(value > 0 ? "+" : "") +
value.toFixed(decimals) +
unit
);
}
function etText(et) {
if (Math.abs(et) < 0.05) {
et = 0;
}
return (
"ET " +
(et > 0 ? "+" : "") +
et.toFixed(1)
);
}
function wheelText(setup) {
return (
setup.width +
"x" +
setup.diameter
);
}
function tyreText(setup) {
return (
Math.round(setup.tyreWidth) +
"/" +
Math.round(setup.profile) +
"R" +
Math.round(setup.diameter)
);
}
function updateDerived(prefix, setup) {
const mode = el(prefix + "Mode").value;
const output = el(prefix + "Derived");
if (mode === "et") {
output.textContent =
"Backspacing ≈ " +
setup.backspacing.toFixed(2) +
" in" +
" · Effective offset " +
etText(setup.effectiveET);
} else {
output.textContent =
"Offset ≈ " +
etText(setup.et) +
" · Effective offset " +
etText(setup.effectiveET);
}
}
function draw(oldSetup, newSetup) {
const oldGroup = el("oldDrawing");
const newGroup = el("newDrawing");
const maximum = Math.max(
oldSetup.innerTyre,
oldSetup.outerTyre,
newSetup.innerTyre,
newSetup.outerTyre,
180
);
const scale =
390 / (maximum + 35);
function inboard(mm) {
return 500 - mm * scale;
}
function outboard(mm) {
return 500 + mm * scale;
}
function wheelDrawing(
setup,
y,
colour,
label
) {
const wheelLeft =
inboard(setup.innerWheel);
const wheelRight =
outboard(setup.outerWheel);
const tyreLeft =
inboard(setup.innerTyre);
const tyreRight =
outboard(setup.outerTyre);
const centre =
500 -
setup.effectiveET *
scale;
return `
<text
x="${tyreLeft + 5}"
y="${y - 8}"
font-size="13"
font-weight="700"
fill="${colour}">
${label}
</text>
<rect
x="${tyreLeft}"
y="${y}"
width="${tyreRight - tyreLeft}"
height="92"
rx="24"
fill="none"
stroke="${colour}"
stroke-width="3">
</rect>
<rect
x="${wheelLeft}"
y="${y + 20}"
width="${wheelRight - wheelLeft}"
height="52"
rx="6"
fill="${colour}"
fill-opacity="0.12"
stroke="${colour}"
stroke-width="3">
</rect>
<line
x1="${centre}"
y1="${y - 3}"
x2="${centre}"
y2="${y + 95}"
stroke="${colour}"
stroke-width="1.5"
stroke-dasharray="6 5">
</line>
<text
x="${tyreRight - 5}"
y="${y + 110}"
text-anchor="end"
font-size="12"
fill="${colour}">
${wheelText(setup)}
·
${etText(setup.et)}
·
${tyreText(setup)}
</text>
`;
}
oldGroup.innerHTML =
wheelDrawing(
oldSetup,
70,
"#8a919a",
"CURRENT"
);
newGroup.innerHTML =
wheelDrawing(
newSetup,
205,
"#c71f2d",
"NEW"
);
}
function calculate() {
const oldSetup =
getSetup("old");
const newSetup =
getSetup("new");
updateDerived(
"old",
oldSetup
);
updateDerived(
"new",
newSetup
);
const innerWheel =
newSetup.innerWheel -
oldSetup.innerWheel;
const outerWheel =
newSetup.outerWheel -
oldSetup.outerWheel;
const innerTyre =
newSetup.innerTyre -
oldSetup.innerTyre;
const outerTyre =
newSetup.outerTyre -
oldSetup.outerTyre;
const diameterDifference =
newSetup.tyreDiameter -
oldSetup.tyreDiameter;
const diameterPercentage =
oldSetup.tyreDiameter
? diameterDifference /
oldSetup.tyreDiameter *
100
: 0;
const rideHeightChange =
diameterDifference / 2;
const speedAt100 =
oldSetup.tyreDiameter
? 100 *
newSetup.tyreDiameter /
oldSetup.tyreDiameter
: 100;
el("innerWheel").textContent =
signed(
innerWheel,
1,
" mm"
);
el("outerWheel").textContent =
signed(
outerWheel,
1,
" mm"
);
el("innerTyre").textContent =
signed(
innerTyre,
1,
" mm"
);
el("outerTyre").textContent =
signed(
outerTyre,
1,
" mm"
);
el("effectiveET").textContent =
etText(
newSetup.effectiveET
);
el("backspacing").textContent =
newSetup.backspacing.toFixed(2) +
" in";
el("diameterChange").textContent =
signed(
diameterPercentage,
2,
"%"
);
el("diameterInfo").textContent =
Math.round(
oldSetup.tyreDiameter
) +
" → " +
Math.round(
newSetup.tyreDiameter
) +
" mm · ride height " +
signed(
rideHeightChange,
1,
" mm"
);
el("speedResult").textContent =
speedAt100.toFixed(1) +
" km/h";
el("summary").innerHTML =
"<strong>" +
wheelText(oldSetup) +
" " +
etText(oldSetup.et) +
" / " +
tyreText(oldSetup) +
"</strong>" +
" → " +
"<strong>" +
wheelText(newSetup) +
" " +
etText(newSetup.et) +
" / " +
tyreText(newSetup) +
"</strong>" +
"<br>" +
"Wheel inner edge: " +
signed(innerWheel, 1, " mm") +
" · outer edge: " +
signed(outerWheel, 1, " mm") +
"<br>" +
"Tyre inner edge: " +
signed(innerTyre, 1, " mm") +
" · outer edge: " +
signed(outerTyre, 1, " mm") +
"<br>" +
"Tyre diameter change: " +
signed(
diameterPercentage,
2,
"%"
) +
" · 100 km/h indicated ≈ " +
speedAt100.toFixed(1) +
" km/h actual.";
draw(
oldSetup,
newSetup
);
}
/*
Normal input changes.
*/
root
.querySelectorAll("input")
.forEach(function(input) {
input.addEventListener(
"input",
calculate
);
});
/*
ET / BS selector.
Keep the same physical wheel position
when changing the displayed input unit.
*/
el("oldMode").addEventListener(
"change",
function() {
syncPosition("old");
calculate();
}
);
el("newMode").addEventListener(
"change",
function() {
syncPosition("new");
calculate();
}
);
/*
If wheel width changes while BS mode is active,
update displayed BS so ET remains unchanged.
*/
el("oldWheelWidth").addEventListener(
"change",
function() {
if (
el("oldMode").value === "bs"
) {
syncPosition("old");
}
calculate();
}
);
el("newWheelWidth").addEventListener(
"change",
function() {
if (
el("newMode").value === "bs"
) {
syncPosition("new");
}
calculate();
}
);
syncPosition("old");
syncPosition("new");
calculate();
})();
</script>
</div>