Build a currency converter with JavaScript in one HTML file

A converter is a table of rates, an amount field, two dropdowns and one line of arithmetic. It runs entirely in the page, so one HTML file is enough.

A currency converter needs no server and no library. Keep a table of rates in your script, read an amount and two currency codes, then divide by the source rate and multiply by the target rate. The version below is about 30 lines.

Try it first. Change the amount or either currency.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Currency converter</title>
<style>
  body { margin: 0; padding: 18px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .box { max-width: 420px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 18px; box-shadow: 0 6px 20px rgba(0,0,0,.1); }
  .row { display: flex; gap: 8px; margin-bottom: 10px; }
  input, select { font: inherit; padding: 9px 10px; border: 1px solid #cfd4dc; border-radius: 8px; min-width: 0; }
  input { flex: 1; }
  select { width: 96px; }
  #out { font-size: 26px; font-weight: 700; margin: 14px 0 4px; }
  small { color: #6b7280; }
</style>
</head>
<body>
<div class="box">
  <div class="row">
    <input id="amount" type="number" value="100" min="0" step="any" aria-label="Amount">
    <select id="from" aria-label="From"></select>
  </div>
  <div class="row">
    <span style="flex:1"></span>
    <select id="to" aria-label="To"></select>
  </div>
  <div id="out"></div>
  <small>Sample rates typed into the page, not live.</small>
</div>

<script>
  // Units of each currency that one US dollar buys (sample numbers)
  const rates = { USD: 1, EUR: 0.92, GBP: 0.79, JPY: 150 };

  const amount = document.getElementById('amount');
  const from = document.getElementById('from');
  const to = document.getElementById('to');
  const out = document.getElementById('out');

  for (const code of Object.keys(rates)) {
    from.add(new Option(code, code));
    to.add(new Option(code, code));
  }
  from.value = 'EUR';
  to.value = 'JPY';

  function convert() {
    const n = amount.valueAsNumber;
    if (!Number.isFinite(n)) { out.textContent = '—'; return; }
    const result = n / rates[from.value] * rates[to.value];  // via the base currency
    out.textContent = new Intl.NumberFormat('en-US', { style: 'currency', currency: to.value }).format(result);
  }

  [amount, from, to].forEach((el) => el.addEventListener('input', convert));
  convert();
</script>
</body>
</html>
The smallest working converter: one amount field, two dropdowns, one formula. The rates are sample numbers, not live prices.

The rates are made-up sample values typed into the code, so the answers are only as good as the numbers you put there. How to keep them current is covered further down.

How the code works, step by step

The four steps of a conversion, using 100 euros to yen with sample rates.
The four steps of a conversion, using 100 euros to yen with sample rates.
  1. Store the rates. One object, one base currency. Each value says how many units of that currency one US dollar buys, and the base itself is 1.
  2. Fill the dropdowns. Loop over Object.keys(rates) and call select.add(new Option(code, code)) for each one. Adding a currency later means adding one line to the object.
  3. Read the amount. valueAsNumber on a number input gives a number, or NaN when the field is empty. Stop early with a dash in that case.
  4. Convert. amount / rates[from] * rates[to] goes through the base currency in two moves.
  5. Format and listen. Print the result with Intl.NumberFormat, and call the same function from an input listener on all three controls.

Convert through one base currency

Storing a rate for every pair does not scale. Five currencies already need twenty numbers, and each one has to stay consistent with the others.

Twenty pair rates against five rows that all point at one base currency.
Twenty pair rates against five rows that all point at one base currency.

With a base, every row answers one question: how much does one dollar buy? Converting 100 euros to yen first divides by 0.92 to get dollars, then multiplies by 150. Euros to pounds, pounds to yen and any other pair use the same line.

The base can be any currency you like. It only has to be the one whose rate is 1.

Read the amount and format the result

The formula is short, so the bugs live around it. Try these three and compare the left column with the right one.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Converter mistakes</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h2 { font-size: 15px; margin: 0 0 8px; }
  .card { background: #fff; border-radius: 12px; padding: 14px; margin-bottom: 14px; box-shadow: 0 4px 14px rgba(0,0,0,.08); }
  input, select { font: inherit; padding: 8px 10px; border: 1px solid #cfd4dc; border-radius: 8px; min-width: 0; }
  .ctl { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
  .ctl input { flex: 1; min-width: 120px; }
  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  .res { border-radius: 8px; padding: 9px 10px; font-size: 14px; }
  .res b { display: block; font-size: 20px; margin-top: 2px; word-break: break-word; }
  .res.bad { background: #fff7f5; border: 1px solid #f3d1c8; }
  .res.good { background: #f4fbf6; border: 1px solid #cfe9d7; }
  code { font: 600 12px ui-monospace, Consolas, monospace; }
</style>
</head>
<body>

<div class="card">
  <h2>1. What people type</h2>
  <div class="ctl"><input id="txt" type="text" value="1,250.5" aria-label="Amount as typed"></div>
  <div class="cols">
    <div class="res bad">parseFloat(text)<b id="t-bad"></b></div>
    <div class="res good">clean first<b id="t-good"></b></div>
  </div>
</div>

<div class="card">
  <h2>2. Which way the rate goes (100 of the first currency)</h2>
  <div class="ctl">
    <select id="from" aria-label="From"></select>
    <select id="to" aria-label="To"></select>
  </div>
  <div class="cols">
    <div class="res bad">amount * rate[from] / rate[to]<b id="d-bad"></b></div>
    <div class="res good">amount / rate[from] * rate[to]<b id="d-good"></b></div>
  </div>
</div>

<div class="card">
  <h2>3. Float noise</h2>
  <div class="cols">
    <div class="res bad">0.1 + 0.2<b id="f-bad"></b></div>
    <div class="res good">rounded for display<b id="f-good"></b></div>
  </div>
</div>

<script>
  const rates = { USD: 1, EUR: 0.92, GBP: 0.79, JPY: 150 };  // sample numbers
  const $ = (id) => document.getElementById(id);

  // 1. text to number
  function cleanNumber(text) {
    const n = Number(text.replace(/[,\s]/g, ''));        // drop thousands separators
    return text.trim() === '' ? NaN : n;                 // Number('') is 0, so treat empty as missing
  }
  function show1() {
    const t = $('txt').value;
    $('t-bad').textContent = String(parseFloat(t));      // stops at the comma: 1
    const c = cleanNumber(t);
    $('t-good').textContent = Number.isFinite(c) ? String(c) : '—';
  }
  $('txt').addEventListener('input', show1);

  // 2. direction
  for (const code of Object.keys(rates)) { $('from').add(new Option(code, code)); $('to').add(new Option(code, code)); }
  $('from').value = 'EUR'; $('to').value = 'JPY';
  function show2() {
    const f = $('from').value, t = $('to').value, n = 100;
    const fmt = (v) => new Intl.NumberFormat('en-US', { style: 'currency', currency: t }).format(v);
    $('d-bad').textContent = fmt(n * rates[f] / rates[t]);
    $('d-good').textContent = fmt(n / rates[f] * rates[t]);
  }
  $('from').addEventListener('input', show2);
  $('to').addEventListener('input', show2);

  // 3. float noise
  $('f-bad').textContent = String(0.1 + 0.2);
  $('f-good').textContent = (0.1 + 0.2).toFixed(2);

  show1(); show2();
</script>
</body>
</html>
Each card shows a common version next to a careful one. Type into the first card and switch currencies in the second.
Straight parseFloat against cleaning the text and checking the result.
Straight parseFloat against cleaning the text and checking the result.
  • Commas. parseFloat reads from the start and stops at the first character that cannot belong to a number, so "1,250.5" becomes 1. A number input avoids the problem. If you use a text field, remove the commas first. That assumes a comma means thousands, which is not true in every locale.
  • Empty fields. An empty number input gives NaN. Test with Number.isFinite and show a dash.
  • Wrong direction. Multiplying by the source rate and dividing by the target rate gives a plausible number that is simply wrong. The second card shows 100 euros turning into about one yen.
  • Rounding. 0.1 + 0.2 is 0.30000000000000004 in JavaScript, because numbers are stored in binary floating point. Keep full precision while you calculate and round once, when you display.

For the output, Intl.NumberFormat with style: 'currency' adds the symbol, the thousands separators and the right number of decimals for the code. Yen has no minor unit, so it prints whole numbers while euros and dollars print two decimals.

const money = (value, code) =>
  new Intl.NumberFormat('en-US', { style: 'currency', currency: code }).format(value);

money(16304.3478, 'JPY');  // "¥16,304"

It formats only. It never converts, so the multiplication stays yours. Pass undefined instead of 'en-US' to use the browser's default locale. The options are covered in Intl.NumberFormat in JavaScript, and reading fields is covered in get an input value in JavaScript.

Where the rates come from

A converter page cannot invent prices, so the numbers come from you. The simplest honest version types them into the object and prints the date next to them, as the examples here do.

A page can also ask a rates service for fresh numbers. That is a request to another origin, and the browser lets your script read the answer only if the server allows it. CORS explains the rule. Keep the typed rates as a fallback:

fetch('https://example.com/rates.json')
  .then((r) => { if (!r.ok) throw new Error(r.status); return r.json(); })
  .then((data) => Object.assign(rates, data))
  .catch(() => {});  // request failed: the typed rates stay

A shared NOS page blocks requests to other sites, so there the catch branch always runs. Treat the typed table as the real data and update it by editing the code.

A finished converter

The same pieces, plus a swap button, a list of the amount in every currency and an editable rate table. Change a rate and every result recalculates.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Currency converter with editable rates</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .box { max-width: 460px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,.1); }
  .top { display: flex; gap: 8px; align-items: center; margin-bottom: 14px; }
  input, select, button { font: inherit; padding: 9px 10px; border: 1px solid #cfd4dc; border-radius: 8px; min-width: 0; background: #fff; }
  #amount { flex: 1; width: 0; }
  button { cursor: pointer; }
  button:hover { background: #eef1f5; }
  #big { font-size: 28px; font-weight: 700; margin: 4px 0 12px; word-break: break-word; }
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
  th, td { text-align: left; padding: 6px 4px; border-bottom: 1px solid #e5e7eb; }
  td.num { text-align: right; font-variant-numeric: tabular-nums; }
  td input { width: 72px; padding: 5px 6px; text-align: right; }
  h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .4px; color: #6b7280; margin: 16px 0 6px; }
  small { color: #6b7280; display: block; margin-top: 10px; }
</style>
</head>
<body>
<div class="box">
  <div class="top">
    <input id="amount" type="number" value="100" min="0" step="any" aria-label="Amount">
    <select id="from" aria-label="From"></select>
    <button id="swap" type="button" aria-label="Swap currencies">&#8644;</button>
    <select id="to" aria-label="To"></select>
  </div>
  <div id="big"></div>

  <h3>Same amount in every currency</h3>
  <table><tbody id="all"></tbody></table>

  <h3>Rates (1 USD buys)</h3>
  <table><tbody id="rates"></tbody></table>
  <small>Sample rates typed into the page on 2026-10-03, not live. Edit a number and every result updates.</small>
</div>

<script>
  const rates = { USD: 1, EUR: 0.92, GBP: 0.79, JPY: 150, KRW: 1350, CAD: 1.36 };  // sample numbers
  const codes = Object.keys(rates);
  const $ = (id) => document.getElementById(id);

  const money = (value, code) =>
    new Intl.NumberFormat('en-US', { style: 'currency', currency: code }).format(value);

  const convert = (n, f, t) => n / rates[f] * rates[t];

  for (const c of codes) { $('from').add(new Option(c, c)); $('to').add(new Option(c, c)); }
  $('from').value = 'EUR';
  $('to').value = 'JPY';

  // editable rate rows
  for (const c of codes) {
    const tr = document.createElement('tr');
    tr.innerHTML = '<td>' + c + '</td><td class="num"><input type="number" step="any" min="0" value="' + rates[c] + '" aria-label="Rate for ' + c + '"></td>';
    const input = tr.querySelector('input');
    if (c === 'USD') input.disabled = true;           // the base stays at 1
    input.addEventListener('input', () => {
      if (input.valueAsNumber > 0) rates[c] = input.valueAsNumber;  // ignore blanks and zero
      render();
    });
    $('rates').appendChild(tr);
  }

  function render() {
    const n = $('amount').valueAsNumber;
    const f = $('from').value, t = $('to').value;
    if (!Number.isFinite(n)) {
      $('big').textContent = '—';
      $('all').innerHTML = '';
      return;
    }
    $('big').textContent = money(n, f) + ' = ' + money(convert(n, f, t), t);
    $('all').innerHTML = codes.map((c) =>
      '<tr><td>' + c + '</td><td class="num">' + money(convert(n, f, c), c) + '</td></tr>').join('');
  }

  $('swap').addEventListener('click', () => {
    const f = $('from').value;
    $('from').value = $('to').value;
    $('to').value = f;
    render();
  });
  ['amount', 'from', 'to'].forEach((id) => $(id).addEventListener('input', render));
  render();
</script>
</body>
</html>
Swap the currencies, edit a rate, or clear the amount. Blank or zero rates are ignored so the maths never divides by zero.
  • Swap: exchange the two select values and call the render function again.
  • One list: map over the currency codes and call the same conversion for each.
  • Editable rates: each input writes back into the object, then calls render.
  • Date label: shows when the numbers were typed, so nobody mistakes them for live prices.

When it does not work

Symptom Cause Fix
Result shows NaN Empty field or text that is not a number Check Number.isFinite first
1,250 becomes 1 parseFloat stops at the comma Remove commas, or use a number input
Euros to yen comes out near 1 Formula is the wrong way round Divide by the source rate, multiply by the target
Result prints 0.30000000000000004 Unrounded floating point Format with Intl.NumberFormat
Dropdowns are empty The fill loop never ran Run it once on load
A rates request fails on a shared page Requests to other sites are blocked Type the rates into the page
Result never updates Listener on the button only Listen for input on the field and both selects

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can change the amount and the currencies themselves. If you update the rates later, the same link shows the new version.

Other pages with the same shape, a few inputs and a result, are covered in calculator to link.

Questions people ask

How do I make a currency converter in JavaScript?

Keep the rates in an object that uses one base currency, fill two select elements from its keys, and read the amount from an input. On every change, divide the amount by the rate of the currency you convert from and multiply by the rate of the currency you convert to. Write the result into the page.

Can the converter use live exchange rates?

A page can request rates from a service, but the browser only lets your script read the answer if that server allows it with CORS headers. In NOS, requests from a shared page to other sites are blocked, so the rates are typed into the code. Edit the numbers and the same share link shows the update.

Why is my converted amount far too small or too large?

The formula is probably the wrong way round. If each rate says how many units one dollar buys, you divide by the rate of the source currency and multiply by the rate of the target currency. Multiplying by the source rate and dividing by the target rate swaps the two.

Does Intl.NumberFormat convert currencies?

No. With style: 'currency' it only chooses the symbol and the usual number of decimals for the code you give it. It prints the number you pass in. The conversion is your own multiplication, done before you format.

Why does my result show NaN?

NaN appears when the code tries to do arithmetic on something that is not a number, usually an empty field or text that parseFloat cannot read. Read the field, check it with Number.isFinite, and show a dash instead of calculating when the check fails.

Keep reading