Example: tabs
The next MAX_TABS sections become tabs, named by each section's first heading, with arrow-key
navigation. It hides Squarespace sections, so it restores them in edit mode. Copy onEditMode
from Squarespace into the same IIFE. Put the constants and init
in the starter in place of its ID line and init. Reads: no attributes.
const ID = "__ID__";
const MAX_TABS = 3;
function init(el) {
const panels = [];
for (let s = el.closest(".page-section")?.nextElementSibling; s && panels.length < MAX_TABS; s = s.nextElementSibling)
if (s.matches(".page-section")) panels.push(s);
if (!panels.length) return void (el.innerHTML = `<p class="sb-empty">Add sections below this block. Each becomes a tab.</p>`);
el.innerHTML = `<div class="sb-tabs" role="tablist"></div>`;
const list = el.firstElementChild, added = [];
const tabs = panels.map((p, i) => {
if (!p.id) { p.id = `sb-${ID}-panel-${i}`; added.push(p); }
const b = document.createElement("button");
Object.assign(b, { type: "button", id: `sb-${ID}-tab-${i}`, className: "sb-tab" });
b.setAttribute("role", "tab"); b.setAttribute("aria-controls", p.id);
b.textContent = p.querySelector("h1, h2, h3, h4")?.textContent.trim().slice(0, 32) || `Tab ${i + 1}`;
p.setAttribute("role", "tabpanel"); p.setAttribute("aria-labelledby", b.id);
p.dataset.sbPanelOf = ID;
return list.appendChild(b);
});
const show = (i) => tabs.forEach((t, j) => {
t.setAttribute("aria-selected", String(i === j));
t.tabIndex = i === j ? 0 : -1;
t.classList.toggle("is-active", i === j);
panels[j].hidden = i !== j;
});
list.addEventListener("click", (e) => { const i = tabs.indexOf(e.target.closest(".sb-tab")); if (i >= 0) show(i); });
list.addEventListener("keydown", (e) => {
const step = { ArrowRight: 1, ArrowLeft: -1 }[e.key];
if (!step) return;
const i = (tabs.indexOf(document.activeElement) + step + tabs.length) % tabs.length;
show(i); tabs[i].focus();
});
show(0);
onEditMode(() => {
for (const p of panels) {
p.hidden = false; p.removeAttribute("role"); p.removeAttribute("aria-labelledby"); delete p.dataset.sbPanelOf;
}
for (const p of added) p.removeAttribute("id");
el.innerHTML = "";
});
}
[data-sb-panel-of="__ID__"][hidden] { display: none !important; }
[data-seamble-id="__ID__"] .sb-tabs { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; padding-bottom: 8px; border-bottom: 2px solid color-mix(in srgb, currentColor 15%, transparent); }
[data-seamble-id="__ID__"] .sb-tab { font: inherit; color: inherit; background: none; border: 0; border-radius: 5px; padding: 8px 16px; min-height: 44px; cursor: pointer; }
[data-seamble-id="__ID__"] .sb-tab:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
[data-seamble-id="__ID__"] .sb-tab.is-active { background: var(--sb-accent, var(--primaryButtonBackgroundColor, #111)); color: var(--sb-on-accent, var(--siteBackgroundColor, #fff)); }
[data-seamble-id="__ID__"] .sb-empty { opacity: 0.6; font-size: 0.9em; }