NNicheStackTutorials
← All tutorials
Frontend Tools

unitbox: One Page, Twelve Unit Categories, Zero React

Travel blog needed km/mi, °C/°F, kg/lb in one static URL. Tabbed converter with shared engine and localStorage last category.

·12 min read·Frontend Tools
unitbox: One Page, Twelve Unit Categories, Zero React cover

Problem: Twelve separate calculator pages killed SEO. One page, category tabs, shared conversion logic.

Full multi-converter (excerpt — length + temperature; pattern repeats)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Unit Converter</title>
  <style>
    body { font-family: system-ui; max-width: 560px; margin: 2rem auto; padding: 0 1rem; }
    .tabs { display: flex; gap: .25rem; flex-wrap: wrap; margin-bottom: 1rem; }
    .tabs button { padding: .35rem .7rem; cursor: pointer; }
    .tabs button.active { background: #0ea5e9; color: #fff; border-color: #0ea5e9; }
    .row { display: flex; gap: .5rem; align-items: center; margin: .5rem 0; }
    input { flex: 1; padding: .5rem; }
    select { padding: .5rem; }
  </style>
</head>
<body>
  <h1>Unit Converter</h1>
  <div class="tabs" id="tabs"></div>
  <div class="row">
    <input type="number" id="val" value="1" step="any">
    <select id="from"></select>
    <span>=</span>
    <input type="number" id="result" readonly>
    <select id="to"></select>
  </div>
  <script>
    const CATEGORIES = {
      length: { m: 1, km: 1000, mi: 1609.344, ft: 0.3048, in: 0.0254 },
      mass: { kg: 1, g: 0.001, lb: 0.453592, oz: 0.0283495 },
      temp: { C: 'temp', F: 'temp', K: 'temp' },
      data: { B: 1, KB: 1024, MB: 1048576, GB: 1073741824 },
    };

    let cat = localStorage.getItem('unitbox-cat') || 'length';
    const tabs = document.getElementById('tabs');
    const from = document.getElementById('from');
    const to = document.getElementById('to');
    const val = document.getElementById('val');
    const result = document.getElementById('result');

    Object.keys(CATEGORIES).forEach(key => {
      const b = document.createElement('button');
      b.textContent = key;
      b.className = key === cat ? 'active' : '';
      b.onclick = () => { cat = key; localStorage.setItem('unitbox-cat', cat); render(); document.querySelectorAll('.tabs button').forEach(x => x.classList.remove('active')); b.classList.add('active'); };
      tabs.appendChild(b);
    });

    function convertTemp(v, fromU, toU) {
      let c = fromU === 'C' ? v : fromU === 'F' ? (v - 32) * 5/9 : v - 273.15;
      if (toU === 'C') return c;
      if (toU === 'F') return c * 9/5 + 32;
      return c + 273.15;
    }

    function convert() {
      const v = Number(val.value);
      const f = from.value, t = to.value;
      if (cat === 'temp') {
        result.value = convertTemp(v, f, t).toFixed(4);
        return;
      }
      const base = CATEGORIES[cat];
      const meters = v * base[f];
      result.value = (meters / base[t]).toFixed(6);
    }

    function render() {
      const units = Object.keys(CATEGORIES[cat]);
      from.innerHTML = to.innerHTML = units.map(u => '<option>' + u + '</option>').join('');
      to.selectedIndex = Math.min(1, units.length - 1);
      convert();
    }

    val.oninput = from.onchange = to.onchange = convert;
    render();
  </script>
</body>
</html>

Temperature is special

Can't multiply by factors — needs offset formulas. Separate branch avoids wrong answers like 20°C = 20°F.

Extend categories

Add volume, speed same pattern — base unit in object, ratio conversion.

Verify

1 km → mi ≈ 0.621371 0°C → 32°F Tab switch remembers via localStorage

Tags: ConverterUnitsVanilla JSStatic Site