What You'll Build

A comprehensive unit converter that handles 8 different categories. Pick a category, choose your "from" and "to" units, type a value — the result updates instantly.

  • 8 categories: Length, Weight, Temperature, Volume, Area, Speed, Time, Data
  • Dynamic dropdowns — units change when you switch category
  • Live conversion as you type
  • Swap button to reverse the units
  • Special handling for temperature (not just multiplication)
▶ Live Preview — this is what you'll build

What You'll Learn

Key new skill: Nested data structures. The whole unit system is described by one JavaScript object. Want to add a new unit? Just add one line of data. The code handles the rest.

1

Build the HTML Structure

Create index.html. The dropdowns are empty — JavaScript fills them from data.

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Unit Converter</title>
</head>
<body>
    <div class="card">
        <h1>Unit Converter</h1>
        <p class="sub">8 categories, dozens of units</p>

        <div class="field">
            <label>Category</label>
            <select id="category">
                <option value="length">Length</option>
                <option value="weight">Weight</option>
                <option value="temperature">Temperature</option>
                <option value="volume">Volume</option>
                <option value="area">Area</option>
                <option value="speed">Speed</option>
                <option value="time">Time</option>
                <option value="data">Data</option>
            </select>
        </div>

        <div class="converter-row">
            <div>
                <label>From</label>
                <input type="number" id="fromValue" value="1" step="any">
                <select id="fromUnit"></select>
            </div>
            <button class="swap-btn" id="swapBtn">Swap</button>
            <div>
                <label>To</label>
                <input type="number" id="toValue" readonly>
                <select id="toUnit"></select>
            </div>
        </div>

        <div class="result">
            <div class="num" id="resultNum">—</div>
            <div class="label" id="resultLabel">Select units and enter a value</div>
        </div>

        <div class="quick-ref" id="quickRef"></div>
    </div>

    <script src="script.js"></script>
</body>
</html>

Key idea: <select id="fromUnit"></select> is empty in the HTML. JavaScript fills it with options. When the user picks a different category, we clear it and refill with new units.

2

Style It with CSS

CSS
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: #f1f5f9;
    padding: 20px;
    color: #0f172a;
    min-height: 100vh;
}

.card {
    max-width: 480px;
    margin: 0 auto;
    background: #fff;
    border-radius: 16px;
    padding: 24px;
    box-shadow: 0 10px 40px rgba(15,23,42,0.08);
}

h1 { font-size: 1.3rem; margin-bottom: 4px; }
.sub { color: #64748b; font-size: 0.85rem; margin-bottom: 16px; }

.field { margin-bottom: 12px; }

label {
    display: block;
    font-size: 0.75rem;
    font-weight: 600;
    color: #334155;
    margin-bottom: 4px;
}

select, input[type="number"] {
    width: 100%;
    padding: 11px 13px;
    border: 2px solid #e2e8f0;
    border-radius: 10px;
    font-size: 0.95rem;
    font-family: inherit;
    outline: none;
    transition: 0.2s;
    font-weight: 600;
}
select:focus, input:focus {
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}

.converter-row {
    display: grid;
    grid-template-columns: 1fr 44px 1fr;
    gap: 8px;
    align-items: end;
    margin-bottom: 12px;
}

.swap-btn {
    background: #f1f5f9;
    border: 2px solid #e2e8f0;
    color: #64748b;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 1.1rem;
    transition: 0.2s;
    margin-bottom: 3px;
}
.swap-btn:hover {
    background: #eff6ff;
    border-color: #3b82f6;
    color: #1d4ed8;
    transform: rotate(180deg);
}

.to-value {
    background: #f8fafc;
    cursor: not-allowed;
    color: #1d4ed8;
}

.result {
    background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
    border-radius: 12px;
    padding: 20px;
    text-align: center;
    color: #fff;
    margin-bottom: 16px;
}
.result .num {
    font-size: 2rem;
    font-weight: 900;
    font-family: "Courier New", monospace;
    line-height: 1;
    word-break: break-all;
}
.result .label {
    font-size: 0.8rem;
    opacity: 0.9;
    margin-top: 8px;
    font-family: "Courier New", monospace;
}

.quick-ref {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 6px;
    margin-top: 12px;
}
.quick-item {
    background: #f8fafc;
    border-radius: 8px;
    padding: 8px 10px;
    font-size: 0.75rem;
    color: #334155;
    cursor: pointer;
    transition: 0.2s;
}
.quick-item:hover { background: #eff6ff; color: #1d4ed8; }
.quick-item strong {
    display: block;
    font-size: 0.7rem;
    color: #64748b;
    margin-bottom: 2px;
    font-weight: 600;
}
3

Write the JavaScript — Data Structure

This is the heart of the project. All conversions are described by one nested object — categories, each with units, each with a conversion factor to a base unit.

JavaScript — Part 1: Data
// ============================================================
// THE DATA — every category, every unit, every factor
// ============================================================
// Structure: category -> { base, units: { "Name (abbr)": factorToBase } }
// To convert: value x fromFactor / toFactor = result

var units = {
    length: {
        name: "Length",
        base: "meter",
        units: {
            "Meter (m)": 1,
            "Kilometer (km)": 1000,
            "Centimeter (cm)": 0.01,
            "Millimeter (mm)": 0.001,
            "Mile (mi)": 1609.344,
            "Yard (yd)": 0.9144,
            "Foot (ft)": 0.3048,
            "Inch (in)": 0.0254
        }
    },
    weight: {
        name: "Weight",
        base: "kilogram",
        units: {
            "Kilogram (kg)": 1,
            "Gram (g)": 0.001,
            "Milligram (mg)": 0.000001,
            "Metric ton (t)": 1000,
            "Pound (lb)": 0.45359237,
            "Ounce (oz)": 0.028349523125,
            "Stone (st)": 6.35029318
        }
    },
    temperature: {
        name: "Temperature",
        // Temperature uses formulas, not factors
        units: {
            "Celsius (°C)": "C",
            "Fahrenheit (°F)": "F",
            "Kelvin (K)": "K"
        }
    },
    volume: {
        name: "Volume",
        base: "liter",
        units: {
            "Liter (L)": 1,
            "Milliliter (mL)": 0.001,
            "Cubic meter (m³)": 1000,
            "US gallon (gal)": 3.785411784,
            "US quart (qt)": 0.946352946,
            "US pint (pt)": 0.473176473,
            "US cup": 0.2365882365,
            "US fluid ounce (fl oz)": 0.0295735295625
        }
    },
    area: {
        name: "Area",
        base: "square meter",
        units: {
            "Square meter (m²)": 1,
            "Square kilometer (km²)": 1000000,
            "Square centimeter (cm²)": 0.0001,
            "Square foot (ft²)": 0.09290304,
            "Square inch (in²)": 0.00064516,
            "Acre": 4046.8564224,
            "Hectare (ha)": 10000
        }
    },
    speed: {
        name: "Speed",
        base: "meter/second",
        units: {
            "Meter/second (m/s)": 1,
            "Kilometer/hour (km/h)": 0.277777778,
            "Mile/hour (mph)": 0.44704,
            "Knot (kn)": 0.4444444,
            "Foot/second (ft/s)": 0.3048
        }
    },
    time: {
        name: "Time",
        base: "second",
        units: {
            "Second (s)": 1,
            "Minute (min)": 60,
            "Hour (h)": 3600,
            "Day (d)": 86400,
            "Week (wk)": 604800,
            "Year (yr)": 31556952
        }
    },
    data: {
        name: "Data",
        base: "byte",
        units: {
            "Bit (b)": 0.125,
            "Byte (B)": 1,
            "Kilobyte (KB)": 1024,
            "Megabyte (MB)": 1048576,
            "Gigabyte (GB)": 1073741824,
            "Terabyte (TB)": 1099511627776
        }
    }
};

// Quick reference presets per category
var quickRefs = {
    length: ["cm to in", "m to ft", "km to mi", "in to cm"],
    weight: ["kg to lb", "lb to kg", "g to oz", "oz to g"],
    temperature: ["C to F", "F to C", "C to K", "K to C"],
    volume: ["L to gal", "gal to L", "mL to fl oz", "cup to mL"],
    area: ["m² to ft²", "ft² to m²", "acre to m²", "ha to acre"],
    speed: ["km/h to mph", "mph to km/h", "m/s to km/h", "knot to mph"],
    time: ["h to min", "min to s", "day to h", "week to day"],
    data: ["MB to GB", "GB to MB", "KB to MB", "bit to byte"]
};

How the data works: Every unit has a number representing "how many base units is 1 of this unit?". A kilometer is 1000 meters, so 1000. An inch is 0.0254 meters, so 0.0254. To convert, multiply by the "from" factor, then divide by the "to" factor.

4

Write the JavaScript — Convert & Populate

JavaScript — Part 2: Logic
// ============================================================
// GRAB DOM ELEMENTS
// ============================================================

var categoryEl = document.getElementById("category");
var fromValueEl = document.getElementById("fromValue");
var toValueEl = document.getElementById("toValue");
var fromUnitEl = document.getElementById("fromUnit");
var toUnitEl = document.getElementById("toUnit");
var resultNumEl = document.getElementById("resultNum");
var resultLabelEl = document.getElementById("resultLabel");
var swapBtn = document.getElementById("swapBtn");
var quickRefEl = document.getElementById("quickRef");

// ============================================================
// POPULATE — fill the two dropdowns based on the category
// ============================================================

function populateUnits() {
    var cat = categoryEl.value;
    var unitKeys = Object.keys(units[cat].units);

    // Clear existing options
    fromUnitEl.innerHTML = "";
    toUnitEl.innerHTML = "";

    // Add an option for each unit
    unitKeys.forEach(function(key) {
        var opt1 = document.createElement("option");
        opt1.value = key;
        opt1.textContent = key;
        fromUnitEl.appendChild(opt1);

        var opt2 = document.createElement("option");
        opt2.value = key;
        opt2.textContent = key;
        toUnitEl.appendChild(opt2);
    });

    // Default: first unit -> second unit
    fromUnitEl.selectedIndex = 0;
    toUnitEl.selectedIndex = Math.min(1, unitKeys.length - 1);
}

// ============================================================
// TEMPERATURE — special case, uses formulas not factors
// ============================================================

function convertTemperature(value, from, to) {
    // Step 1: Convert everything to Celsius first
    var celsius;
    if (from.indexOf("Celsius") === 0) {
        celsius = value;
    } else if (from.indexOf("Fahrenheit") === 0) {
        celsius = (value - 32) * 5 / 9;
    } else {
        celsius = value - 273.15;
    }

    // Step 2: Convert Celsius to the target unit
    if (to.indexOf("Celsius") === 0) return celsius;
    if (to.indexOf("Fahrenheit") === 0) return (celsius * 9 / 5) + 32;
    return celsius + 273.15;
}

// ============================================================
// FORMAT NUMBER — handle very large and very small values
// ============================================================

function formatNumber(n) {
    if (!isFinite(n)) return "—";
    if (Math.abs(n) >= 1e12) return n.toExponential(4);
    if (Math.abs(n) >= 1e6) return n.toLocaleString(undefined, { maximumFractionDigits: 2 });
    if (Math.abs(n) >= 1) return n.toLocaleString(undefined, { maximumFractionDigits: 6 });
    if (Math.abs(n) >= 0.001) return n.toFixed(6).replace(/\.?0+$/, "");
    return n.toExponential(4);
}

// ============================================================
// CONVERT — the main function
// ============================================================

function convert() {
    var cat = categoryEl.value;
    var fromU = fromUnitEl.value;
    var toU = toUnitEl.value;
    var val = parseFloat(fromValueEl.value);

    if (isNaN(val)) {
        toValueEl.value = "";
        resultNumEl.textContent = "—";
        resultLabelEl.textContent = "Enter a value to convert";
        return;
    }

    var result;
    if (cat === "temperature") {
        result = convertTemperature(val, fromU, toU);
    } else {
        var fromFactor = units[cat].units[fromU];
        var toFactor = units[cat].units[toU];
        result = (val * fromFactor) / toFactor;
    }

    var formatted = formatNumber(result);
    toValueEl.value = formatted;
    resultNumEl.textContent = formatted;
    resultLabelEl.textContent = val + " " + fromU + " = " + formatted + " " + toU;
}
5

Write the JavaScript — Quick Refs & Events

JavaScript — Part 3: Events
// ============================================================
// QUICK REFERENCE — clickable preset conversions
// ============================================================

function renderQuickRefs() {
    var cat = categoryEl.value;
    var refs = quickRefs[cat] || [];

    quickRefEl.innerHTML = refs.map(function(r) {
        return '<div class="quick-item" data-ref="' + r + '">' +
               '<strong>Quick</strong>' + r +
               '</div>';
    }).join("");

    // Attach click listener to each preset
    quickRefEl.querySelectorAll(".quick-item").forEach(function(item) {
        item.addEventListener("click", function() {
            loadQuickRef(item.dataset.ref);
        });
    });
}

function loadQuickRef(ref) {
    var parts = ref.split(" to ");
    if (parts.length !== 2) return;

    var cat = categoryEl.value;
    var unitKeys = Object.keys(units[cat].units);

    // Find the units that match the preset text
    var fromKey = unitKeys.find(function(k) {
        return k.toLowerCase().indexOf(parts[0].toLowerCase()) !== -1;
    });
    var toKey = unitKeys.find(function(k) {
        return k.toLowerCase().indexOf(parts[1].toLowerCase()) !== -1;
    });

    if (fromKey) fromUnitEl.value = fromKey;
    if (toKey) toUnitEl.value = toKey;
    fromValueEl.value = 1;
    convert();
}

// ============================================================
// EVENTS — wire up all interactions
// ============================================================

// Category change — rebuild dropdowns and quick refs
categoryEl.addEventListener("change", function() {
    populateUnits();
    renderQuickRefs();
    convert();
});

// Unit changes — reconvert
fromUnitEl.addEventListener("change", convert);
toUnitEl.addEventListener("change", convert);

// Value input — live update
fromValueEl.addEventListener("input", convert);

// Swap button — flip from/to
swapBtn.addEventListener("click", function() {
    var fi = fromUnitEl.selectedIndex;
    var ti = toUnitEl.selectedIndex;
    fromUnitEl.selectedIndex = ti;
    toUnitEl.selectedIndex = fi;
    convert();
});

// ============================================================
// BOOT — populate everything on load
// ============================================================

populateUnits();
renderQuickRefs();
convert();

Save all files and open index.html. Pick a category, choose units, type a value — the result updates instantly. Try the swap button and the quick-reference presets.

6

Understand the Big Ideas

1. Nested data structures

The entire converter is defined by one object:

JavaScript
{
  category: {
    name: "...",
    base: "...",
    units: {
      "Unit Name": factorToBase,
      ...
    }
  }
}

This pattern — an object whose values are also objects — appears everywhere in real code. A shop has products in categories. A social app has users in groups. A game has items in inventories. Learning to navigate nested data is essential.

2. Why multiply-then-divide works

Every unit's factor is "how many base units in 1 of this unit". So:

Example: 5 miles → km. Miles factor is 1609.344 meters per mile. So 5 × 1609.344 = 8046.72 meters. Then ÷ 1000 (km factor) = 8.047 km. Works for any combination.

3. Why temperature is special

Temperature scales aren't proportional — 0°C is not "zero temperature", it's just the freezing point of water. So you can't use a multiplier. Instead, we convert everything to Celsius, then to the target — the same trick used for currency conversion in real apps.

Generalizable: This "convert to a common base, then to the target" pattern works for currencies, time zones, and any system with an offset. Keep it in your toolbox.

4. Dynamic dropdowns

We never hardcoded any <option> tags. They're built with document.createElement("option") and appendChild(). This means adding a new unit is one line of data — no HTML changes.

7

Practice Challenges

🟢 Beginner

🟡 Intermediate

🔴 Advanced

Compare your solution: We built the production version with 8 full categories and more units — see the live Unit Converter.

Full Source Code

Common Mistakes & Fixes

❌ "Dropdowns are empty"

You forgot to call populateUnits() on page load. The dropdowns start empty in the HTML — JavaScript must fill them.

❌ "Changing category doesn't update the units"

You forgot to add the change event listener on the category dropdown that calls populateUnits().

❌ "Temperature conversions are wrong"

You're treating temperature like other units (with a factor). Temperature needs the special formula — check convertTemperature().

❌ "Very small numbers show as 0"

JavaScript floats are limited in precision. That's why formatNumber() uses exponential notation for tiny values. For extreme precision you'd need a decimal library, but for learning this is fine.

❌ "Swap button doesn't work"

Make sure you're swapping the selectedIndex of the two dropdowns, not the dropdown elements themselves.

What to Build Next