Content & Media

Free Podcast Player Widget

A podcast episode list with a built-in audio player.

A compact podcast player that renders your episode list from a JSON attribute and plays episodes right on the page, with a play/pause button, seek bar, and auto-advance to the next episode. Great for podcasters, creators, and blogs that want a listen-on-site option without a hosted embed. Paste one script tag where you want it to appear 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/podcast-player.js" data-hl-config='[{"title":"Ep. 12 — Shipping a Client Portal in a Weekend","date":"Jul 28, 2026","src":"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3","duration":"6:12"},{"title":"Ep. 11 — Pricing Creative Work Without Guessing","date":"Jul 21, 2026","src":"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3","duration":"7:04"},{"title":"Ep. 10 — What Small Businesses Get Wrong About SEO","date":"Jul 14, 2026","src":"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3","duration":"5:44"}]' data-hl-title="The Build Log" async></script>
<!-- Free Podcast Player widget by haynes lab — https://hayneslab.dev/tools/widgets/podcast-player -->
Inline (self-contained)html
<!-- Free Podcast Player widget by haynes lab — https://hayneslab.dev/tools/widgets/podcast-player -->
<script data-hl-config='[{"title":"Ep. 12 — Shipping a Client Portal in a Weekend","date":"Jul 28, 2026","src":"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3","duration":"6:12"},{"title":"Ep. 11 — Pricing Creative Work Without Guessing","date":"Jul 21, 2026","src":"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3","duration":"7:04"},{"title":"Ep. 10 — What Small Businesses Get Wrong About SEO","date":"Jul 14, 2026","src":"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3","duration":"5:44"}]' data-hl-title="The Build Log">
/*!
 * haynes lab — Podcast Player widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/podcast-player
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-podcast-player';
  var STYLE_ID = 'hlw-podcast-player-styles';

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 560px; margin: 0 auto; padding: 24px 24px 28px;' +
      ' 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 + '__header { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 16px; }' +
      '.' + PREFIX + '__title { margin: 0; font-size: 18px; font-weight: 700; color: #111827; }' +
      '.' + PREFIX + '__count { font-size: 12px; color: #9ca3af; white-space: nowrap; }' +
      '.' + PREFIX + '__player { display: flex; align-items: center; gap: 14px; padding: 12px 14px;' +
      ' background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; margin-bottom: 16px; }' +
      '.' + PREFIX + '__play { flex: 0 0 auto; width: 44px; height: 44px; border: 0; border-radius: 50%;' +
      ' background: var(--hlw-accent); color: #ffffff; cursor: pointer; padding: 0;' +
      ' display: flex; align-items: center; justify-content: center; }' +
      '.' + PREFIX + '__play:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__play svg { display: block; }' +
      '.' + PREFIX + '__now { flex: 1 1 auto; min-width: 0; }' +
      '.' + PREFIX + '__now-title { font-weight: 600; font-size: 14px; color: #111827;' +
      ' white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }' +
      '.' + PREFIX + '__progress { height: 6px; margin-top: 8px; background: #e5e7eb; border-radius: 999px;' +
      ' cursor: pointer; position: relative; }' +
      '.' + PREFIX + '__progress-fill { position: absolute; inset: 0 auto 0 0; width: 0%;' +
      ' background: var(--hlw-accent); border-radius: 999px; }' +
      '.' + PREFIX + '__time { display: flex; justify-content: space-between; margin-top: 5px;' +
      ' font-size: 11px; color: #9ca3af; font-variant-numeric: tabular-nums; }' +
      '.' + PREFIX + '__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }' +
      '.' + PREFIX + '__episode { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;' +
      ' padding: 10px 12px; border: 1px solid transparent; border-radius: 8px; background: none;' +
      ' font: inherit; color: inherit; cursor: pointer; }' +
      '.' + PREFIX + '__episode:hover { background: #f9fafb; }' +
      '.' + PREFIX + '__episode:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__episode[aria-current="true"] { background: #f9fafb; border-color: #e5e7eb; }' +
      '.' + PREFIX + '__num { flex: 0 0 auto; width: 24px; font-size: 12px; color: #9ca3af;' +
      ' font-variant-numeric: tabular-nums; text-align: right; }' +
      '.' + PREFIX + '__episode[aria-current="true"] .' + PREFIX + '__num { color: var(--hlw-accent); font-weight: 700; }' +
      '.' + PREFIX + '__meta { flex: 1 1 auto; min-width: 0; }' +
      '.' + PREFIX + '__ep-title { display: block; font-weight: 600; font-size: 14px; color: #111827;' +
      ' white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }' +
      '.' + PREFIX + '__ep-date { display: block; font-size: 12px; color: #6b7280; margin-top: 1px; }' +
      '.' + PREFIX + '__duration { flex: 0 0 auto; font-size: 12px; color: #9ca3af; font-variant-numeric: tabular-nums; }' +
      '.' + 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: 18px 16px 26px; }' +
      ' .' + PREFIX + '__duration { display: none; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function parseEpisodes(script) {
    var raw = script.getAttribute('data-hl-config');
    if (!raw) {
      console.warn('[haynes lab podcast-player] Missing required data-hl-config attribute (JSON array of {title, date, src, duration?}). Nothing rendered.');
      return null;
    }
    var items;
    try {
      items = JSON.parse(raw);
    } catch (e) {
      console.warn('[haynes lab podcast-player] data-hl-config is not valid JSON. Nothing rendered.', e);
      return null;
    }
    if (!Array.isArray(items)) {
      console.warn('[haynes lab podcast-player] data-hl-config must be a JSON array. Nothing rendered.');
      return null;
    }
    var valid = items.filter(function (ep) {
      return ep && typeof ep.title === 'string' && ep.title && typeof ep.src === 'string' && ep.src;
    });
    if (!valid.length) {
      console.warn('[haynes lab podcast-player] data-hl-config has no valid episodes (each needs at least "title" and "src"). Nothing rendered.');
      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 iconSvg(kind) {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('width', '18');
    svg.setAttribute('height', '18');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    if (kind === 'play') {
      var p = document.createElementNS(svgNS, 'path');
      p.setAttribute('d', 'M8 5v14l11-7z');
      p.setAttribute('fill', 'currentColor');
      svg.appendChild(p);
    } else {
      var r1 = document.createElementNS(svgNS, 'rect');
      r1.setAttribute('x', '6'); r1.setAttribute('y', '5');
      r1.setAttribute('width', '4'); r1.setAttribute('height', '14');
      r1.setAttribute('fill', 'currentColor');
      var r2 = document.createElementNS(svgNS, 'rect');
      r2.setAttribute('x', '14'); r2.setAttribute('y', '5');
      r2.setAttribute('width', '4'); r2.setAttribute('height', '14');
      r2.setAttribute('fill', 'currentColor');
      svg.appendChild(r1);
      svg.appendChild(r2);
    }
    return svg;
  }

  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 fmtTime(sec) {
    if (!isFinite(sec) || sec < 0) return '0:00';
    var m = Math.floor(sec / 60);
    var s = Math.floor(sec % 60);
    return m + ':' + (s < 10 ? '0' : '') + s;
  }

  function build(script) {
    var items = parseEpisodes(script);
    if (!items) return;

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

    injectStyles();

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

    var header = el('div', PREFIX + '__header');
    header.appendChild(el('h3', PREFIX + '__title', title));
    header.appendChild(el('span', PREFIX + '__count',
      items.length + (items.length === 1 ? ' episode' : ' episodes')));
    root.appendChild(header);

    var audio = document.createElement('audio');
    audio.preload = 'metadata';

    var player = el('div', PREFIX + '__player');
    var playBtn = el('button', PREFIX + '__play');
    playBtn.type = 'button';
    playBtn.setAttribute('aria-label', 'Play');
    playBtn.appendChild(iconSvg('play'));

    var now = el('div', PREFIX + '__now');
    var nowTitle = el('div', PREFIX + '__now-title', 'Select an episode');
    var progress = el('div', PREFIX + '__progress');
    progress.setAttribute('role', 'progressbar');
    progress.setAttribute('aria-label', 'Playback progress');
    var fill = el('div', PREFIX + '__progress-fill');
    progress.appendChild(fill);
    var time = el('div', PREFIX + '__time');
    var curTime = el('span', null, '0:00');
    var totTime = el('span', null, '0:00');
    time.appendChild(curTime);
    time.appendChild(totTime);
    now.appendChild(nowTitle);
    now.appendChild(progress);
    now.appendChild(time);
    player.appendChild(playBtn);
    player.appendChild(now);
    root.appendChild(player);

    var list = el('ol', PREFIX + '__list');
    root.appendChild(list);

    var current = -1;

    function setPlaying(playing) {
      playBtn.textContent = '';
      playBtn.appendChild(iconSvg(playing ? 'pause' : 'play'));
      playBtn.setAttribute('aria-label', playing ? 'Pause' : 'Play');
    }

    function load(i, autoplay) {
      if (i === current) {
        if (autoplay && audio.paused) audio.play();
        return;
      }
      current = i;
      audio.src = items[i].src;
      nowTitle.textContent = items[i].title;
      Array.prototype.forEach.call(list.children, function (li, liIdx) {
        li.firstChild.setAttribute('aria-current', liIdx === i ? 'true' : 'false');
      });
      if (autoplay) {
        var p = audio.play();
        if (p && p.catch) p.catch(function () { setPlaying(false); });
      }
    }

    items.forEach(function (ep, i) {
      var li = document.createElement('li');
      var btn = el('button', PREFIX + '__episode');
      btn.type = 'button';
      btn.setAttribute('aria-current', 'false');
      btn.setAttribute('aria-label', 'Play episode: ' + ep.title);
      btn.appendChild(el('span', PREFIX + '__num', String(i + 1)));
      var meta = el('span', PREFIX + '__meta');
      meta.appendChild(el('span', PREFIX + '__ep-title', ep.title));
      if (ep.date) meta.appendChild(el('span', PREFIX + '__ep-date', String(ep.date)));
      btn.appendChild(meta);
      if (ep.duration) btn.appendChild(el('span', PREFIX + '__duration', String(ep.duration)));
      btn.addEventListener('click', function () {
        if (i === current && !audio.paused) {
          audio.pause();
        } else {
          load(i, true);
        }
      });
      li.appendChild(btn);
      list.appendChild(li);
    });

    playBtn.addEventListener('click', function () {
      if (current === -1) { load(0, true); return; }
      if (audio.paused) audio.play(); else audio.pause();
    });

    progress.addEventListener('click', function (e) {
      if (!isFinite(audio.duration) || !audio.duration) return;
      var rect = progress.getBoundingClientRect();
      var frac = (e.clientX - rect.left) / rect.width;
      audio.currentTime = Math.max(0, Math.min(1, frac)) * audio.duration;
    });

    audio.addEventListener('play', function () { setPlaying(true); });
    audio.addEventListener('pause', function () { setPlaying(false); });
    audio.addEventListener('loadedmetadata', function () {
      totTime.textContent = fmtTime(audio.duration);
    });
    audio.addEventListener('timeupdate', function () {
      curTime.textContent = fmtTime(audio.currentTime);
      if (isFinite(audio.duration) && audio.duration) {
        fill.style.width = (audio.currentTime / audio.duration * 100) + '%';
      }
    });
    audio.addEventListener('ended', function () {
      if (current + 1 < items.length) load(current + 1, true);
    });

    load(0, false);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/podcast-player';
    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 podcast-player] 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(required)Episode list as [{"title":"...","date":"...","src":"...","duration":"..."}] where src is the audio file URL and duration is optional display text like "24:15".
data-hl-title"Podcast"Heading shown above the episode list.
data-hl-accent"#f97316"Any CSS color used for the play button, progress bar, and active episode.

Frequently asked questions

How do I install the podcast player?

Paste the embed snippet where you want the player to appear and put your episodes in the data-hl-config attribute as [{"title":"...","date":"...","src":"..."}] with src pointing at each audio file URL. The widget renders right where the script tag sits, and the first episode loads ready to play.

Can I customize the title and colors?

Yes. Use data-hl-title for the heading text and data-hl-accent for the play button, progress bar, and active-episode highlight. Everything else is styled neutrally to fit any light website.

Does it work on WordPress, Shopify, or Squarespace?

Yes. The player is a single dependency-free vanilla JS file with its own scoped styles and a standard HTML5 audio element. 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