Finance & Investing

Free Currency Converter Widget

A live fiat currency converter with your own exchange rates.

A clean currency converter with an amount input, from/to currency selects, and a live-updating result. You supply the rates as JSON (or point it at your own rates endpoint), so you control exactly what visitors see. Paste one script tag anywhere on your site and it works.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/currency-converter.js" data-hl-config='{"base":"USD","rates":{"EUR":0.92,"GBP":0.79,"JPY":149.50,"CAD":1.36,"AUD":1.52,"CHF":0.88,"CNY":7.24,"MXN":17.10}}' data-hl-default-from="USD" data-hl-default-to="EUR" async></script>
<!-- Free Currency Converter widget by haynes lab — https://hayneslab.dev/tools/widgets/currency-converter -->
Inline (self-contained)html
<!-- Free Currency Converter widget by haynes lab — https://hayneslab.dev/tools/widgets/currency-converter -->
<script data-hl-config='{"base":"USD","rates":{"EUR":0.92,"GBP":0.79,"JPY":149.50,"CAD":1.36,"AUD":1.52,"CHF":0.88,"CNY":7.24,"MXN":17.10}}' data-hl-default-from="USD" data-hl-default-to="EUR">
/*!
 * haynes lab — Currency Converter widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/currency-converter
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-currency-converter';
  var STYLE_ID = 'hlw-currency-converter-styles';

  // Fallback sample rates, used only when neither data-hl-config nor
  // data-hl-endpoint is supplied. Owners should always provide their own.
  var SAMPLE = {
    base: 'USD',
    rates: { EUR: 0.92, GBP: 0.79, JPY: 149.5, CAD: 1.36, AUD: 1.52, CHF: 0.88, CNY: 7.24, MXN: 17.1 }
  };

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/currency-converter.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 420px; margin: 0 auto; padding: 24px 24px 30px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; }' +
      '.' + PREFIX + '__label { display: block; font-size: 12px; font-weight: 600;' +
      ' text-transform: uppercase; letter-spacing: 0.04em; color: #6b7280; margin-bottom: 6px; }' +
      '.' + PREFIX + '__amount { width: 100%; padding: 10px 12px; font-size: 20px; font-weight: 600;' +
      ' color: #111827; border: 1px solid #d1d5db; border-radius: 8px; background: #ffffff;' +
      ' font-family: inherit; margin-bottom: 14px; }' +
      '.' + PREFIX + '__amount:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px; border-color: var(--hlw-accent); }' +
      '.' + PREFIX + '__row { display: flex; align-items: flex-end; gap: 10px; }' +
      '.' + PREFIX + '__field { flex: 1 1 0; min-width: 0; }' +
      '.' + PREFIX + '__select { width: 100%; padding: 10px 12px; font-size: 15px; color: #111827;' +
      ' border: 1px solid #d1d5db; border-radius: 8px; background: #ffffff; font-family: inherit; cursor: pointer; }' +
      '.' + PREFIX + '__select:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px; border-color: var(--hlw-accent); }' +
      '.' + PREFIX + '__swap { flex: 0 0 auto; width: 38px; height: 38px; margin-bottom: 1px; padding: 0;' +
      ' border: 1px solid #e5e7eb; border-radius: 50%; background: #ffffff; color: #6b7280;' +
      ' font-size: 17px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; }' +
      '.' + PREFIX + '__swap:hover { color: var(--hlw-accent); border-color: var(--hlw-accent); }' +
      '.' + PREFIX + '__swap:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__result { margin-top: 18px; padding: 14px 16px; border-radius: 8px;' +
      ' background: #f9fafb; border: 1px solid #f3f4f6; }' +
      '.' + PREFIX + '__converted { font-size: 24px; font-weight: 700; color: var(--hlw-accent);' +
      ' word-break: break-word; }' +
      '.' + PREFIX + '__rate { font-size: 13px; color: #6b7280; margin-top: 4px; }' +
      '.' + PREFIX + '__note { font-size: 11px; color: #9ca3af; margin-top: 8px; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 6px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  // Normalizes a rates payload into { base, rates } with numeric values.
  function normalizeData(data) {
    if (!data || typeof data.base !== 'string' || !data.base ||
        !data.rates || typeof data.rates !== 'object') return null;
    var rates = {};
    for (var code in data.rates) {
      var n = parseFloat(data.rates[code]);
      if (isFinite(n) && n > 0) rates[code.toUpperCase()] = n;
    }
    var codes = Object.keys(rates);
    if (!codes.length) return null;
    return { base: data.base.toUpperCase(), rates: rates };
  }

  function formatAmount(value) {
    return value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  }

  function build(script, data, isSample) {
    var accent = script.getAttribute('data-hl-accent') || '#f97316';
    var defaultFrom = (script.getAttribute('data-hl-default-from') || data.base).toUpperCase();
    var defaultTo = (script.getAttribute('data-hl-default-to') || '').toUpperCase();

    var codes = [data.base].concat(Object.keys(data.rates));
    if (codes.indexOf(defaultFrom) === -1) defaultFrom = data.base;
    if (codes.indexOf(defaultTo) === -1) {
      defaultTo = codes.filter(function (c) { return c !== defaultFrom; })[0] || data.base;
    }

    injectStyles();

    var root = el('div', PREFIX);
    root.style.setProperty('--hlw-accent', accent);

    var amountLabel = el('label', PREFIX + '__label', 'Amount');
    var amountId = PREFIX + '-amount-' + Math.random().toString(36).slice(2, 8);
    amountLabel.setAttribute('for', amountId);
    var amount = el('input', PREFIX + '__amount');
    amount.type = 'number';
    amount.id = amountId;
    amount.min = '0';
    amount.step = 'any';
    amount.value = '100';
    root.appendChild(amountLabel);
    root.appendChild(amount);

    var row = el('div', PREFIX + '__row');

    function makeField(labelText, selected) {
      var field = el('div', PREFIX + '__field');
      var label = el('label', PREFIX + '__label', labelText);
      var select = el('select', PREFIX + '__select');
      var id = PREFIX + '-select-' + Math.random().toString(36).slice(2, 8);
      label.setAttribute('for', id);
      select.id = id;
      codes.forEach(function (code) {
        var opt = el('option', null, code);
        opt.value = code;
        if (code === selected) opt.selected = true;
        select.appendChild(opt);
      });
      field.appendChild(label);
      field.appendChild(select);
      return { field: field, select: select };
    }

    var from = makeField('From', defaultFrom);
    var to = makeField('To', defaultTo);

    var swap = el('button', PREFIX + '__swap', '⇄');
    swap.type = 'button';
    swap.setAttribute('aria-label', 'Swap currencies');

    row.appendChild(from.field);
    row.appendChild(swap);
    row.appendChild(to.field);
    root.appendChild(row);

    var result = el('div', PREFIX + '__result');
    result.setAttribute('aria-live', 'polite');
    var converted = el('div', PREFIX + '__converted');
    var rate = el('div', PREFIX + '__rate');
    result.appendChild(converted);
    result.appendChild(rate);
    root.appendChild(result);

    var note = null;
    if (isSample) {
      note = el('div', PREFIX + '__note', 'Showing sample rates — supply your own via data-hl-config.');
      root.appendChild(note);
    }

    function rateOf(code) {
      return code === data.base ? 1 : data.rates[code];
    }

    function update() {
      var value = parseFloat(amount.value);
      var f = from.select.value;
      var t = to.select.value;
      var cross = rateOf(t) / rateOf(f);
      if (!isFinite(value) || value < 0) {
        converted.textContent = '—';
        rate.textContent = 'Enter an amount to convert.';
        return;
      }
      converted.textContent = formatAmount(value * cross) + ' ' + t;
      rate.textContent = '1 ' + f + ' = ' + formatAmount(cross) + ' ' + t;
    }

    amount.addEventListener('input', update);
    from.select.addEventListener('change', update);
    to.select.addEventListener('change', update);
    swap.addEventListener('click', function () {
      var tmp = from.select.value;
      from.select.value = to.select.value;
      to.select.value = tmp;
      update();
    });
    update();

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/currency-converter';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab currency-converter] Could not locate its own script tag. Nothing rendered.');
      return;
    }

    var raw = script.getAttribute('data-hl-config');
    var endpoint = script.getAttribute('data-hl-endpoint');

    if (raw) {
      var parsed = null;
      try {
        parsed = normalizeData(JSON.parse(raw));
      } catch (e) {
        console.warn('[haynes lab currency-converter] data-hl-config is not valid JSON. Falling back to sample rates.', e);
      }
      if (!parsed && !endpoint) {
        console.warn('[haynes lab currency-converter] data-hl-config must look like {"base":"USD","rates":{"EUR":0.92,...}}. Falling back to sample rates.');
      }
      if (parsed) { build(script, parsed, false); return; }
      if (!endpoint) { build(script, SAMPLE, true); return; }
    }

    if (endpoint) {
      fetch(endpoint)
        .then(function (res) {
          if (!res.ok) throw new Error('HTTP ' + res.status);
          return res.json();
        })
        .then(function (json) {
          var data = normalizeData(json);
          if (!data) throw new Error('Unexpected rates payload shape.');
          build(script, data, false);
        })
        .catch(function (err) {
          console.warn('[haynes lab currency-converter] Could not load rates from data-hl-endpoint. Falling back to sample rates.', err);
          build(script, SAMPLE, true);
        });
      return;
    }

    console.warn('[haynes lab currency-converter] No data-hl-config or data-hl-endpoint supplied. Rendering with sample rates.');
    build(script, SAMPLE, true);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-config(none)Exchange rates as {"base":"USD","rates":{"EUR":0.92,...}} where each rate is 1 base unit in that currency. You supply and update the rates yourself.
data-hl-endpoint(none)URL returning the same {"base","rates"} JSON shape; fetched on page load. Ignored when data-hl-config is present and valid.
data-hl-default-from(base currency)Currency code preselected in the "From" select. Falls back to the base currency.
data-hl-default-to(first other currency)Currency code preselected in the "To" select. Falls back to the first listed currency other than "from".
data-hl-accent"#f97316"Any CSS color used for the converted result and focus states.

Frequently asked questions

How do I install the currency converter?

Paste the embed snippet where you want the converter to appear and put your exchange rates in the data-hl-config attribute as {"base":"USD","rates":{"EUR":0.92,...}}. The widget renders right where the script tag sits.

Where do the exchange rates come from?

You supply them. Either inline via data-hl-config or from your own URL via data-hl-endpoint (any service or file that returns the same {"base","rates"} JSON). That keeps the widget dependency-free and lets you update rates on your own schedule. If you provide neither, it renders with clearly-labeled sample rates.

Does it work on WordPress, Shopify, or Squarespace?

Yes. The converter is a single dependency-free vanilla JS file with its own scoped styles. Drop the script tag into a Custom HTML block on any platform, or straight into your page HTML.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch