Finance & Investing

Free Exchange Rate Table Widget

A clean, embeddable table of currency pairs, rates, and daily change.

Exchange Rate Table renders a tidy, mobile-friendly table of currency pairs with their rates and an optional change column. It is built for finance blogs, travel sites, and business pages that want to show FX rates without a heavy plugin. Paste one script tag with your own rates in a JSON attribute, or point it at a JSON endpoint for live data.

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/exchange-rate-table.js" data-hl-config='[{"pair":"EUR/USD","rate":1.0842,"change":0.18},{"pair":"GBP/USD","rate":1.2715,"change":-0.11},{"pair":"USD/JPY","rate":155.32,"change":0.42},{"pair":"USD/CHF","rate":0.9048,"change":-0.07},{"pair":"AUD/USD","rate":0.6591,"change":0.25},{"pair":"USD/CAD","rate":1.3687,"change":-0.15}]' data-hl-title="Major Currency Pairs" async></script>
<!-- Free Exchange Rate Table widget by haynes lab — https://hayneslab.dev/tools/widgets/exchange-rate-table -->
Inline (self-contained)html
<!-- Free Exchange Rate Table widget by haynes lab — https://hayneslab.dev/tools/widgets/exchange-rate-table -->
<script data-hl-config='[{"pair":"EUR/USD","rate":1.0842,"change":0.18},{"pair":"GBP/USD","rate":1.2715,"change":-0.11},{"pair":"USD/JPY","rate":155.32,"change":0.42},{"pair":"USD/CHF","rate":0.9048,"change":-0.07},{"pair":"AUD/USD","rate":0.6591,"change":0.25},{"pair":"USD/CAD","rate":1.3687,"change":-0.15}]' data-hl-title="Major Currency Pairs">
/*!
 * haynes lab — Exchange Rate Table widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/exchange-rate-table
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-exchange-rate-table';
  var STYLE_ID = 'hlw-exchange-rate-table-styles';

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 520px; margin: 0 auto; padding: 20px 20px 26px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 15px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; }' +
      '.' + PREFIX + '__title { margin: 0 0 12px; font-size: 17px; font-weight: 700; color: #111827;' +
      ' display: flex; align-items: center; gap: 8px; }' +
      '.' + PREFIX + '__title::before { content: ""; display: block; width: 4px; height: 18px;' +
      ' border-radius: 2px; background: var(--hlw-accent); flex: 0 0 auto; }' +
      '.' + PREFIX + '__scroll { overflow-x: auto; }' +
      '.' + PREFIX + '__table { width: 100%; border-collapse: collapse; }' +
      '.' + PREFIX + '__th { text-align: left; font-size: 11px; font-weight: 600; letter-spacing: 0.06em;' +
      ' text-transform: uppercase; color: #6b7280; padding: 6px 10px; border-bottom: 1px solid #e5e7eb; }' +
      '.' + PREFIX + '__th--num, .' + PREFIX + '__td--num { text-align: right; }' +
      '.' + PREFIX + '__td { padding: 9px 10px; border-bottom: 1px solid #f3f4f6; color: #374151;' +
      ' font-variant-numeric: tabular-nums; }' +
      '.' + PREFIX + '__row:last-child .' + PREFIX + '__td { border-bottom: 0; }' +
      '.' + PREFIX + '__pair { font-weight: 600; color: #111827; }' +
      '.' + PREFIX + '__up { color: #16a34a; }' +
      '.' + PREFIX + '__down { color: #dc2626; }' +
      '.' + PREFIX + '__flat { color: #6b7280; }' +
      '.' + PREFIX + '__status { padding: 18px 10px; text-align: center; color: #6b7280; font-size: 14px; }' +
      '.' + 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; }' +
      '@media (max-width: 480px) { .' + PREFIX + ' { padding: 16px 14px 24px; font-size: 14px; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function validate(items, source) {
    if (!Array.isArray(items)) {
      console.warn('[haynes lab exchange-rate-table] ' + source + ' must be a JSON array of {"pair","rate","change"}. Nothing rendered.');
      return null;
    }
    var valid = items.filter(function (r) {
      return r && typeof r.pair === 'string' && r.pair &&
        (typeof r.rate === 'number' || typeof r.rate === 'string') && r.rate !== '';
    });
    if (!valid.length) {
      console.warn('[haynes lab exchange-rate-table] ' + source + ' has no valid rows (each needs "pair" and "rate"). Nothing rendered.');
      return null;
    }
    return valid;
  }

  function parseConfig(script) {
    var raw = script.getAttribute('data-hl-config');
    if (!raw) return null;
    var items;
    try {
      items = JSON.parse(raw);
    } catch (e) {
      console.warn('[haynes lab exchange-rate-table] data-hl-config is not valid JSON. Nothing rendered.', e);
      return null;
    }
    return validate(items, 'data-hl-config');
  }

  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;
  }

  function formatRate(rate) {
    if (typeof rate === 'string') return rate;
    return rate.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 });
  }

  function renderRows(script, scroll) {
    var items = parseConfig(script);
    if (!items) return;

    scroll.textContent = '';
    var showChange = items.some(function (r) {
      return r.change !== undefined && r.change !== null && r.change !== '';
    });

    var table = el('table', PREFIX + '__table');
    var thead = document.createElement('thead');
    var headRow = document.createElement('tr');
    headRow.appendChild(el('th', PREFIX + '__th', 'Pair'));
    headRow.appendChild(el('th', PREFIX + '__th ' + PREFIX + '__th--num', 'Rate'));
    if (showChange) headRow.appendChild(el('th', PREFIX + '__th ' + PREFIX + '__th--num', 'Change'));
    thead.appendChild(headRow);
    table.appendChild(thead);

    var tbody = document.createElement('tbody');
    items.forEach(function (r) {
      var tr = el('tr', PREFIX + '__row');
      tr.appendChild(el('td', PREFIX + '__td ' + PREFIX + '__pair', r.pair));
      tr.appendChild(el('td', PREFIX + '__td ' + PREFIX + '__td--num', formatRate(r.rate)));
      if (showChange) {
        var num = parseFloat(r.change);
        var cell = el('td', PREFIX + '__td ' + PREFIX + '__td--num');
        if (isNaN(num)) {
          cell.classList.add(PREFIX + '__flat');
          cell.textContent = '—';
        } else {
          var arrow = num > 0 ? '▲ ' : (num < 0 ? '▼ ' : '');
          cell.classList.add(num > 0 ? PREFIX + '__up' : (num < 0 ? PREFIX + '__down' : PREFIX + '__flat'));
          cell.textContent = arrow + (num > 0 ? '+' : '') + num + '%';
        }
        tr.appendChild(cell);
      }
      tbody.appendChild(tr);
    });
    table.appendChild(tbody);
    scroll.appendChild(table);
  }

  function build(script) {
    var endpoint = script.getAttribute('data-hl-endpoint');
    if (!script.getAttribute('data-hl-config') && !endpoint) {
      console.warn('[haynes lab exchange-rate-table] Provide data-hl-config (JSON array) or data-hl-endpoint. Nothing rendered.');
      return;
    }

    var title = script.getAttribute('data-hl-title') || 'Exchange Rates';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    injectStyles();

    var root = el('div', PREFIX);
    root.style.setProperty('--hlw-accent', accent);
    root.appendChild(el('h3', PREFIX + '__title', title));

    var scroll = el('div', PREFIX + '__scroll');
    root.appendChild(scroll);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/exchange-rate-table';
    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);
    }

    if (script.getAttribute('data-hl-config')) {
      renderRows(script, scroll);
    } else {
      scroll.appendChild(el('div', PREFIX + '__status', 'Loading rates…'));
      fetch(endpoint)
        .then(function (res) {
          if (!res.ok) throw new Error('HTTP ' + res.status);
          return res.json();
        })
        .then(function (data) {
          var rows = validate(data, 'data-hl-endpoint response');
          if (!rows) {
            scroll.textContent = '';
            scroll.appendChild(el('div', PREFIX + '__status', 'Rates unavailable.'));
            return;
          }
          script.setAttribute('data-hl-config', JSON.stringify(rows));
          renderRows(script, scroll);
        })
        .catch(function (e) {
          console.warn('[haynes lab exchange-rate-table] Failed to load data-hl-endpoint.', e);
          scroll.textContent = '';
          scroll.appendChild(el('div', PREFIX + '__status', 'Rates unavailable.'));
        });
    }
  }

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

  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)JSON array of {"pair","rate","change"} objects; change is optional and hides the column when omitted on all rows.
data-hl-endpoint(none)URL returning the same JSON array shape; fetched live when data-hl-config is not set.
data-hl-title"Exchange Rates"Heading shown above the table.
data-hl-accent"#f97316"CSS color used for the title accent bar.

Frequently asked questions

How do I install the exchange rate table?

Paste the script tag with a data-hl-config attribute where you want the table to appear. It renders inline right after the script tag — no other setup needed.

Can I show live rates instead of static JSON?

Yes. Set data-hl-endpoint to a URL that returns a JSON array of {"pair","rate","change"} objects and the widget fetches it on page load. You can also set data-hl-title and data-hl-accent to match your brand.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies, so it works anywhere you can paste a script tag, including WordPress, Shopify, Squarespace, and plain HTML sites.

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