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)
📏 Unit Converter
8 categories, dozens of units
What You'll Learn
- Nested objects: a data structure where each category contains its own units and conversion factors
- Dynamic dropdowns: populating select elements from data instead of hardcoding options
- Category switching: changing one dropdown rebuilds two others
- Special-case logic: temperature doesn't just multiply — it uses formulas
- Data-driven thinking: adding a new category requires only a data change, no new code
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.
Build the HTML Structure
Create index.html. The dropdowns are empty — JavaScript fills them from data.
<!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.
Style It with 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;
}
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.
// ============================================================
// 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.
Write the JavaScript — Convert & Populate
// ============================================================
// 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;
}
Write the JavaScript — Quick Refs & 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.
Understand the Big Ideas
1. Nested data structures
The entire converter is defined by one object:
{
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:
- value × fromFactor converts to the base unit
- Then ÷ toFactor converts to the target
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.
Practice Challenges
🟢 Beginner
- Add a copy result button
- Add a new unit — for example, "Nautical mile (nmi)" in Length
- Add a new category — try "Energy" or "Pressure"
🟡 Intermediate
- Show all units at once — a table below showing the value in every unit
- Add keyboard support — number pad, arrow keys to change units
- Save the last-used units and value to
localStorage
🔴 Advanced
- Add URL parameters —
?cat=length&from=Meter (m)&to=Inch (in)&v=5 - Add a history panel showing the last 10 conversions
- Make each category its own page for SEO
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.