Free Tabs Widget
Accessible, keyboard-friendly tabbed panels for any page content.
A free tabs widget that turns a simple JSON list of titles and text into clean, accessible tabbed panels on your site. It is built for bloggers, docs pages, and product pages that need to organize content without heavy plugins. Paste one script tag with your data and the tabs appear exactly where you place it.
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.
<script src="https://hayneslab.dev/widgets/tabs.js" data-hl-config='[{"title":"Overview","text":"Tabs keep long pages tidy by grouping related content into switchable panels. Visitors see one section at a time without scrolling."},{"title":"Features","text":"Fully keyboard accessible with arrow-key navigation, proper ARIA roles, and a single script tag install. No dependencies, no framework required."},{"title":"Use Cases","text":"Great for product details, service breakdowns, FAQs, pricing explanations, or any page where you want to organize content into clean sections."}]' async></script>
<!-- Free Tabs widget by haynes lab — https://hayneslab.dev/tools/widgets/tabs --><!-- Free Tabs widget by haynes lab — https://hayneslab.dev/tools/widgets/tabs -->
<script data-hl-config='[{"title":"Overview","text":"Tabs keep long pages tidy by grouping related content into switchable panels. Visitors see one section at a time without scrolling."},{"title":"Features","text":"Fully keyboard accessible with arrow-key navigation, proper ARIA roles, and a single script tag install. No dependencies, no framework required."},{"title":"Use Cases","text":"Great for product details, service breakdowns, FAQs, pricing explanations, or any page where you want to organize content into clean sections."}]'>
/*!
* haynes lab — Tabs widget (free)
* Docs & embed code: https://hayneslab.dev/tools/widgets/tabs
*/
(function () {
'use strict';
var PREFIX = 'hlw-tabs';
var STYLE_ID = 'hlw-tabs-styles';
var uid = 0;
function findScript() {
return document.currentScript ||
document.querySelector('script[src*="widgets/tabs.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: 24px 24px 32px;' +
' 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 + '__list { display: flex; flex-wrap: wrap; gap: 4px; margin: 0 0 20px;' +
' border-bottom: 1px solid #e5e7eb; }' +
'.' + PREFIX + '__tab { appearance: none; border: 0; background: transparent; padding: 10px 16px;' +
' margin-bottom: -1px; font: inherit; font-size: 15px; font-weight: 500; color: #6b7280;' +
' cursor: pointer; border-bottom: 2px solid transparent; border-radius: 6px 6px 0 0; }' +
'.' + PREFIX + '__tab:hover { color: #111827; background: #f9fafb; }' +
'.' + PREFIX + '__tab[aria-selected="true"] { color: var(--hlw-accent);' +
' border-bottom-color: var(--hlw-accent); }' +
'.' + PREFIX + '__tab:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: -2px; }' +
'.' + PREFIX + '__panel { color: #374151; }' +
'.' + PREFIX + '__panel[hidden] { display: none; }' +
'.' + PREFIX + '__panel:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
'.' + 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: 560px) { .' + PREFIX + ' { padding: 16px 16px 30px; }' +
' .' + PREFIX + '__tab { padding: 8px 12px; font-size: 14px; } }';
var style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = css;
document.head.appendChild(style);
}
function parseTabs(script) {
var raw = script.getAttribute('data-hl-config');
if (!raw) {
console.warn('[haynes lab tabs] Missing required data-hl-config attribute (JSON array of {title, text}). Nothing rendered.');
return null;
}
var items;
try {
items = JSON.parse(raw);
} catch (e) {
console.warn('[haynes lab tabs] data-hl-config is not valid JSON. Nothing rendered.', e);
return null;
}
if (!Array.isArray(items)) {
console.warn('[haynes lab tabs] data-hl-config must be a JSON array. Nothing rendered.');
return null;
}
var valid = items.filter(function (t) {
return t && typeof t.title === 'string' && t.title && typeof t.text === 'string' && t.text;
});
if (!valid.length) {
console.warn('[haynes lab tabs] data-hl-config has no valid tabs (each needs at least "title" and "text"). 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 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 items = parseTabs(script);
if (!items) return;
var accent = script.getAttribute('data-hl-accent') || '#f97316';
uid += 1;
injectStyles();
var root = el('div', PREFIX);
root.style.setProperty('--hlw-accent', accent);
var list = el('div', PREFIX + '__list');
list.setAttribute('role', 'tablist');
root.appendChild(list);
var tabs = [];
var panels = [];
var active = 0;
function select(i, focus) {
active = (i + items.length) % items.length;
tabs.forEach(function (tab, ti) {
var on = ti === active;
tab.setAttribute('aria-selected', on ? 'true' : 'false');
tab.tabIndex = on ? 0 : -1;
panels[ti].hidden = !on;
});
if (focus) tabs[active].focus();
}
items.forEach(function (t, i) {
var id = PREFIX + '-' + uid + '-' + i;
var tab = el('button', PREFIX + '__tab', t.title);
tab.type = 'button';
tab.id = id + '-tab';
tab.setAttribute('role', 'tab');
tab.setAttribute('aria-controls', id + '-panel');
var panel = el('div', PREFIX + '__panel', t.text);
panel.id = id + '-panel';
panel.setAttribute('role', 'tabpanel');
panel.setAttribute('aria-labelledby', id + '-tab');
panel.tabIndex = 0;
tab.addEventListener('click', function () { select(i, false); });
tab.addEventListener('keydown', function (e) {
var next = null;
if (e.key === 'ArrowRight') next = active + 1;
else if (e.key === 'ArrowLeft') next = active - 1;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = items.length - 1;
if (next !== null) {
e.preventDefault();
select(next, true);
}
});
tabs.push(tab);
panels.push(panel);
list.appendChild(tab);
root.appendChild(panel);
});
select(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/tabs';
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 tabs] 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.
| Attribute | Default | Description |
|---|---|---|
data-hl-config | (required) | JSON array of tabs, each an object with "title" and "text" (plain text content). |
data-hl-accent | "#f97316" | Color used for the active tab underline and focus outlines. |
Frequently asked questions
How do I install the tabs?
Paste the embed snippet where you want the tabs to appear — the widget renders inline right after the script tag, wherever you place it on the page.
How do I customize the tabs?
Set your tabs as a JSON array in data-hl-config (each with "title" and "text"), and change the highlight color with the data-hl-accent attribute.
Does it work on WordPress, Shopify, or Squarespace?
Yes — it is a single vanilla JS file with no dependencies, so it works on any platform that lets you add a custom HTML or code block.
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