Navigation & Accessibility

Free Breadcrumb Trail Widget

Accessible breadcrumb navigation from your URL path or simple JSON.

An accessible breadcrumb navigation bar that either renders a trail you define with a small JSON config or auto-builds itself from the current page’s URL path. It is ideal for blogs, docs, e-commerce catalogs, and any site with nested pages. Install it with a single script tag wherever you want the trail to appear.

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/breadcrumb-trail.js" data-hl-config='[{"label":"Home","href":"/"},{"label":"Blog","href":"/blog/"},{"label":"Guides","href":"/blog/guides/"},{"label":"Speed Up Your Site"}]' async></script>
<!-- Free Breadcrumb Trail widget by haynes lab — https://hayneslab.dev/tools/widgets/breadcrumb-trail -->
Inline (self-contained)html
<!-- Free Breadcrumb Trail widget by haynes lab — https://hayneslab.dev/tools/widgets/breadcrumb-trail -->
<script data-hl-config='[{"label":"Home","href":"/"},{"label":"Blog","href":"/blog/"},{"label":"Guides","href":"/blog/guides/"},{"label":"Speed Up Your Site"}]'>
/*!
 * haynes lab — Breadcrumb Trail widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/breadcrumb-trail
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-breadcrumb-trail';
  var STYLE_ID = 'hlw-breadcrumb-trail-styles';

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 720px; margin: 0 auto; padding: 10px 16px 22px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 14px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; }' +
      '.' + PREFIX + '__list { display: flex; flex-wrap: wrap; align-items: center; gap: 2px;' +
      ' list-style: none; margin: 0; padding: 0; }' +
      '.' + PREFIX + '__item { display: inline-flex; align-items: center; min-width: 0; }' +
      '.' + PREFIX + '__link { color: #4b5563; text-decoration: none; padding: 2px 4px; border-radius: 4px;' +
      ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }' +
      '.' + PREFIX + '__link:hover { color: var(--hlw-accent); text-decoration: underline; }' +
      '.' + PREFIX + '__link:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 1px;' +
      ' text-decoration: none; }' +
      '.' + PREFIX + '__sep { color: #9ca3af; padding: 0 4px; user-select: none; }' +
      '.' + PREFIX + '__current { color: #111827; font-weight: 600; padding: 2px 4px;' +
      ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 5px; 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 titleCase(segment) {
    var text;
    try {
      text = decodeURIComponent(segment);
    } catch (e) {
      text = segment;
    }
    text = text.replace(/\.[a-z0-9]+$/i, '').replace(/[-_]+/g, ' ').trim();
    if (!text) return '';
    return text.replace(/\b\w/g, function (ch) { return ch.toUpperCase(); });
  }

  function crumbsFromPath(homeLabel) {
    var crumbs = [{ label: homeLabel, href: '/' }];
    var segments = window.location.pathname.split('/').filter(function (s) { return s; });
    var path = '';
    segments.forEach(function (segment, i) {
      path += '/' + segment;
      var label = titleCase(segment);
      if (!label) return;
      crumbs.push({ label: label, href: i === segments.length - 1 ? null : path + '/' });
    });
    return crumbs;
  }

  function parseCrumbs(script, homeLabel) {
    var raw = script.getAttribute('data-hl-config');
    if (!raw) return crumbsFromPath(homeLabel);
    var items;
    try {
      items = JSON.parse(raw);
    } catch (e) {
      console.warn('[haynes lab breadcrumb-trail] data-hl-config is not valid JSON. Falling back to auto-built trail from the current URL path.', e);
      return crumbsFromPath(homeLabel);
    }
    if (!Array.isArray(items)) {
      console.warn('[haynes lab breadcrumb-trail] data-hl-config must be a JSON array of {"label","href"}. Falling back to auto-built trail.');
      return crumbsFromPath(homeLabel);
    }
    var valid = items.filter(function (c) {
      return c && typeof c.label === 'string' && c.label;
    }).map(function (c) {
      return { label: c.label, href: typeof c.href === 'string' && c.href ? c.href : null };
    });
    if (!valid.length) {
      console.warn('[haynes lab breadcrumb-trail] data-hl-config has no valid crumbs (each needs a "label"). Falling back to auto-built trail.');
      return crumbsFromPath(homeLabel);
    }
    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 build(script) {
    var separator = script.getAttribute('data-hl-separator') || '›';
    var homeLabel = script.getAttribute('data-hl-home-label') || 'Home';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    var crumbs = parseCrumbs(script, homeLabel);
    if (crumbs.length < 2 && !script.getAttribute('data-hl-config')) return;

    injectStyles();

    var root = el('nav', PREFIX);
    root.setAttribute('aria-label', 'Breadcrumb');
    root.style.setProperty('--hlw-accent', accent);

    var list = el('ol', PREFIX + '__list');
    crumbs.forEach(function (crumb, i) {
      var isLast = i === crumbs.length - 1;
      var item = el('li', PREFIX + '__item');
      if (isLast) {
        var current = el('span', PREFIX + '__current', crumb.label);
        current.setAttribute('aria-current', 'page');
        item.appendChild(current);
      } else if (crumb.href) {
        var link = el('a', PREFIX + '__link', crumb.label);
        link.href = crumb.href;
        item.appendChild(link);
      } else {
        item.appendChild(el('span', PREFIX + '__current', crumb.label));
      }
      if (!isLast) {
        var sep = el('span', PREFIX + '__sep', separator);
        sep.setAttribute('aria-hidden', 'true');
        item.appendChild(sep);
      }
      list.appendChild(item);
    });
    root.appendChild(list);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/breadcrumb-trail';
    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 breadcrumb-trail] 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)Optional JSON array of {"label","href"} objects; when omitted the trail is auto-built from the current URL path.
data-hl-separator"›"Character shown between crumbs, e.g. /, ›, or →.
data-hl-home-label"Home"Label used for the first crumb when the trail is auto-built from the URL path.
data-hl-accent"#f97316"Highlight color for link hover and focus states.

Frequently asked questions

How do I install the Breadcrumb Trail?

Paste the script snippet where you want the trail to appear — it renders an accessible nav with an ordered list right after the tag. With no config it builds itself from the current URL path; add data-hl-config to define the crumbs yourself.

Can I customize the separator and labels?

Yes — set data-hl-separator to any character (default ›), data-hl-home-label for the first crumb in auto mode, and data-hl-accent for the hover and focus color. The current page always gets aria-current="page" for screen readers.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies, so it works on WordPress, Shopify, Squarespace, or any site that can embed a script tag.

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