Finance & Investing

Free Stock Ticker Tape Widget

An auto-scrolling market ticker tape with prices and % change for any website.

A classic auto-scrolling stock ticker tape showing symbols, prices, and percent change in green or red, pausing on hover. There is no free CORS-safe live finance API, so you supply the data yourself — as inline JSON or from any endpoint you host. Install is one script tag, no dependencies.

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/stock-ticker-tape.js" data-hl-config='[{"symbol":"AAPL","price":214.10,"change":1.24},{"symbol":"MSFT","price":428.55,"change":0.87},{"symbol":"GOOGL","price":178.32,"change":-0.42},{"symbol":"AMZN","price":205.74,"change":1.63},{"symbol":"NVDA","price":131.88,"change":3.05},{"symbol":"META","price":585.20,"change":-1.12},{"symbol":"TSLA","price":248.42,"change":2.31},{"symbol":"JPM","price":224.80,"change":0.18}]' async></script>
<!-- Free Stock Ticker Tape widget by haynes lab — https://hayneslab.dev/tools/widgets/stock-ticker-tape -->
Inline (self-contained)html
<!-- Free Stock Ticker Tape widget by haynes lab — https://hayneslab.dev/tools/widgets/stock-ticker-tape -->
<script data-hl-config='[{"symbol":"AAPL","price":214.10,"change":1.24},{"symbol":"MSFT","price":428.55,"change":0.87},{"symbol":"GOOGL","price":178.32,"change":-0.42},{"symbol":"AMZN","price":205.74,"change":1.63},{"symbol":"NVDA","price":131.88,"change":3.05},{"symbol":"META","price":585.20,"change":-1.12},{"symbol":"TSLA","price":248.42,"change":2.31},{"symbol":"JPM","price":224.80,"change":0.18}]'>
/*!
 * haynes lab — Stock Ticker Tape widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/stock-ticker-tape
 *
 * DATA: there is no free CORS-safe live finance API, so this widget never
 * fetches quotes on its own. Supply your own feed via data-hl-config (inline
 * JSON array of {"symbol","price","change"}) and/or data-hl-endpoint (a URL
 * you host that returns that same JSON, fetched once, then re-fetched every
 * data-hl-refresh seconds if set). With neither, sample data is shown.
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-stock-ticker-tape';
  var STYLE_ID = 'hlw-stock-ticker-tape-styles';

  var SAMPLE = [
    { symbol: 'AAPL', price: 214.10, change: 1.24 },
    { symbol: 'MSFT', price: 428.55, change: 0.87 },
    { symbol: 'GOOGL', price: 178.32, change: -0.42 },
    { symbol: 'AMZN', price: 205.74, change: 1.63 },
    { symbol: 'NVDA', price: 131.88, change: 3.05 },
    { symbol: 'META', price: 585.20, change: -1.12 },
    { symbol: 'TSLA', price: 248.42, change: 2.31 },
    { symbol: 'JPM', price: 224.80, change: 0.18 }
  ];

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 14px; line-height: 1.4; color: #1f2937; }' +
      '.' + PREFIX + '__tape { position: relative; overflow: hidden; background: #111827; border-radius: 8px; }' +
      '.' + PREFIX + '__track { display: inline-flex; align-items: center; white-space: nowrap;' +
      ' will-change: transform; animation: ' + PREFIX + '-scroll linear infinite; }' +
      '.' + PREFIX + '__tape:hover .' + PREFIX + '__track,' +
      '.' + PREFIX + '__tape:focus-within .' + PREFIX + '__track { animation-play-state: paused; }' +
      '@keyframes ' + PREFIX + '-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }' +
      '@media (prefers-reduced-motion: reduce) { .' + PREFIX + '__track { animation: none; } }' +
      '.' + PREFIX + '__item { display: inline-flex; align-items: baseline; gap: 8px; padding: 10px 20px; }' +
      '.' + PREFIX + '__symbol { font-weight: 700; letter-spacing: 0.04em; color: #f9fafb; }' +
      '.' + PREFIX + '__price { color: #d1d5db; font-variant-numeric: tabular-nums; }' +
      '.' + PREFIX + '__change { font-weight: 600; font-variant-numeric: tabular-nums; }' +
      '.' + PREFIX + '__change--up { color: #4ade80; }' +
      '.' + PREFIX + '__change--down { color: #f87171; }' +
      '.' + PREFIX + '__change--flat { color: #9ca3af; }' +
      '.' + PREFIX + '__credit { display: flex; justify-content: flex-end; align-items: center; gap: 4px;' +
      ' margin-top: 4px; font-size: 11px; 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 parseItems(raw, source) {
    var items;
    try {
      items = JSON.parse(raw);
    } catch (e) {
      console.warn('[haynes lab stock-ticker-tape] ' + source + ' is not valid JSON.', e);
      return null;
    }
    if (!Array.isArray(items)) {
      console.warn('[haynes lab stock-ticker-tape] ' + source + ' must be a JSON array of {"symbol","price","change"}.');
      return null;
    }
    var valid = items.filter(function (t) {
      return t && typeof t.symbol === 'string' && t.symbol &&
        t.price !== undefined && t.price !== null && !isNaN(Number(t.price));
    });
    if (!valid.length) {
      console.warn('[haynes lab stock-ticker-tape] ' + source + ' has no valid entries (each needs at least "symbol" and "price").');
      return null;
    }
    return valid;
  }

  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 formatPrice(value) {
    var n = Number(value);
    return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  }

  function build(script) {
    var speed = parseFloat(script.getAttribute('data-hl-speed'));
    if (isNaN(speed) || speed <= 0) speed = 60;
    var endpoint = script.getAttribute('data-hl-endpoint');
    var refresh = parseInt(script.getAttribute('data-hl-refresh'), 10);
    if (isNaN(refresh) || refresh < 5) refresh = 0;

    var items = null;
    var raw = script.getAttribute('data-hl-config');
    if (raw) items = parseItems(raw, 'data-hl-config');
    if (!items && !endpoint) {
      console.warn('[haynes lab stock-ticker-tape] No data-hl-config or data-hl-endpoint supplied — showing sample data. Provide your own feed for real quotes.');
      items = SAMPLE;
    }

    injectStyles();

    var root = el('div', PREFIX);
    root.setAttribute('role', 'region');
    root.setAttribute('aria-label', 'Stock ticker tape');
    var tape = el('div', PREFIX + '__tape');
    var track = el('div', PREFIX + '__track');
    track.setAttribute('aria-hidden', 'true');
    tape.appendChild(track);
    root.appendChild(tape);

    function renderTrack() {
      track.textContent = '';
      // Render the list twice so the -50% translateX loop is seamless.
      for (var copy = 0; copy < 2; copy++) {
        items.forEach(function (t) {
          var item = el('span', PREFIX + '__item');
          item.appendChild(el('span', PREFIX + '__symbol', t.symbol.toUpperCase()));
          item.appendChild(el('span', PREFIX + '__price', formatPrice(t.price)));
          var change = Number(t.change) || 0;
          var dir = change > 0 ? 'up' : change < 0 ? 'down' : 'flat';
          var arrow = change > 0 ? '▲' : change < 0 ? '▼' : '•';
          var sign = change > 0 ? '+' : '';
          item.appendChild(el('span', PREFIX + '__change ' + PREFIX + '__change--' + dir,
            arrow + ' ' + sign + change.toFixed(2) + '%'));
          track.appendChild(item);
        });
      }
      // Duration from measured width keeps the px/second speed consistent.
      track.style.animationDuration = '0s';
      requestAnimationFrame(function () {
        var half = track.scrollWidth / 2;
        track.style.animationDuration = half > 0 ? (half / speed).toFixed(2) + 's' : '30s';
      });
    }

    function applyItems(next) {
      if (next) {
        items = next;
        renderTrack();
      }
    }

    if (endpoint) {
      var load = function () {
        fetch(endpoint, { credentials: 'omit' })
          .then(function (res) {
            if (!res.ok) throw new Error('HTTP ' + res.status);
            return res.text();
          })
          .then(function (text) {
            applyItems(parseItems(text, 'data-hl-endpoint response'));
          })
          .catch(function (err) {
            console.warn('[haynes lab stock-ticker-tape] Failed to load data-hl-endpoint.', err);
          });
      };
      load();
      if (refresh) setInterval(load, refresh * 1000);
    }

    if (items) renderTrack();

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/stock-ticker-tape';
    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 stock-ticker-tape] 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)Inline JSON array of {"symbol","price","change"} entries (change is percent); sample mega-cap data is shown when this and data-hl-endpoint are both omitted.
data-hl-endpoint(none)URL you host that returns the same JSON array as data-hl-config; fetched once on load and overrides the inline entries.
data-hl-refresh(none)When set to 5 or more, re-fetches data-hl-endpoint on this interval to update the tape.
data-hl-speed"60"How fast the tape scrolls, in pixels per second; higher values move faster.

Frequently asked questions

How do I install the stock ticker tape?

Paste the script snippet where you want the tape to appear — it renders inline right after the tag. Add your entries with data-hl-config or point data-hl-endpoint at your own JSON feed.

Where does the stock data come from?

You supply it. There is no free CORS-safe live finance API, so the widget never fetches quotes on its own. Provide inline JSON via data-hl-config ([{"symbol","price","change"}]) and/or a URL you host via data-hl-endpoint, optionally refreshed every data-hl-refresh seconds. Without either, sample mega-cap data is shown. Control scroll speed with data-hl-speed.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies. Paste the script tag into any HTML block, code injection area, or theme template and the tape appears inline at that spot.

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