Tabs
Install
Section titled “Install”pnpm add @agencecinq/tabsImport once:
import "@agencecinq/tabs";Then write the WAI‑ARIA markup:
<cinq-tabs data-tabs-hash="false" data-tabs-delay="0"> <div role="tablist" aria-label="Example tabs"> <button id="tab-home" role="tab" aria-selected="true" aria-controls="panel-home" > Home </button> <button id="tab-project" role="tab" aria-selected="false" aria-controls="panel-project" tabindex="-1" > Project </button> </div>
<section id="panel-home" role="tabpanel" aria-labelledby="tab-home" tabindex="0" > … </section> <section id="panel-project" role="tabpanel" aria-labelledby="tab-project" tabindex="0" hidden > … </section></cinq-tabs>Options
Section titled “Options”Configure via data attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
data-tabs-hash | true/false | true | Sync the active tab id to location.hash on activation. |
data-tabs-delay | number (ms) | 0 | When > 0, arrow key navigation activates the focused tab after the given delay. |
Methods
Section titled “Methods”| Method | Description |
|---|---|
activateTab(index: number) | Activates a tab programmatically (useful after canceling tab-before-activate). |
Events
Section titled “Events”| Event | Cancelable | Detail payload | Description |
|---|---|---|---|
tab-before-activate | Yes | { index, controls, element } | Fired before activation; call event.preventDefault() to override. |
tab-activate | No | { controls, element } | Fired when a tab has been activated. |
tab-delete | No | { controls, element } | Fired when a deletable tab has been removed. |
Examples
Section titled “Examples”Automatic activation (data-tabs-delay)
Section titled “Automatic activation (data-tabs-delay)”When data-tabs-delay is greater than 0, moving focus with arrow keys will automatically activate the tab after the given delay (in ms):
<cinq-tabs data-tabs-hash="true" data-tabs-delay="300"> <div role="tablist" aria-label="Monster tabs"> <button id="tab-troll" role="tab" aria-selected="true" aria-controls="panel-troll" > Troll </button> <button id="tab-dracolich" role="tab" aria-selected="false" aria-controls="panel-dracolich" tabindex="-1" > Dracolich </button> </div>
<section id="panel-troll" role="tabpanel" aria-labelledby="tab-troll" tabindex="0" > … </section> <section id="panel-dracolich" role="tabpanel" aria-labelledby="tab-dracolich" tabindex="0" hidden > … </section></cinq-tabs>Example
Section titled “Example”Below, data-tabs-delay="300" means that when you move focus with Left/Right arrows, the tab will auto‑activate after 300 ms:
Trolls regenerate quickly; automatic activation makes cycling through their stats fast with only the arrow keys.
The dracolich panel becomes active after a short delay once focus moves to its tab.
Async loading with tab-before-activate
Section titled “Async loading with tab-before-activate”const tabsEl = document.querySelector("cinq-tabs");
tabsEl.addEventListener("tab-before-activate", event => { const { index, controls } = event.detail;
// Cancel the built-in activation event.preventDefault();
// Simulate async work (fetch data, animate, etc.) fetch(`/api/panels/${controls}`) .then(response => response.json()) .then(() => { // When ready, activate the tab programmatically tabsEl.activateTab(index); });});Example
Section titled “Example”The live demo below wires tab-before-activate to the public D&D 5e monsters API and only activates the tab once the monster data has been fetched and rendered:
Adult Black Dragon
Loading from D&D API…
This panel will be populated from a public D&D monsters API on first activation.
Ancient Gold Dragon
Loading from D&D API…
Open this tab to fetch another monster payload.
Destroy & recreate a <cinq-tabs> instance
Section titled “Destroy & recreate a <cinq-tabs> instance”const tabsEl = document.querySelector("cinq-tabs.js-tabs");const destroyBtn = document.querySelector(".js-tabs-destroy");const createBtn = document.querySelector(".js-tabs-create");
destroyBtn?.addEventListener("click", () => { tabsEl.destroy();});
createBtn?.addEventListener("click", () => { tabsEl.init();});Example
Section titled “Example”Destroy / Create
Useful when you mutate the DOM and want to re-bind events.
Destroy removes ARIA state + listeners, Create re-initializes.
Try keyboard navigation after Create.
Add tabs at runtime
Section titled “Add tabs at runtime”const tabsEl = document.querySelector("cinq-tabs.js-tabs-add");const list = tabsEl.querySelector('[role="tablist"]');const panels = tabsEl.querySelector("[data-tab-panels]");const addBtn = document.querySelector(".js-tabs-add");const labelInput = document.querySelector(".js-tabs-label");const contentInput = document.querySelector(".js-tabs-content");
addBtn?.addEventListener("click", () => { const label = labelInput.value.trim(); const content = contentInput.value.trim(); if (!label || !content) return;
const id = label.toLowerCase().replace(/\s+/g, "-");
tabsEl.destroy();
list.insertAdjacentHTML( "beforeend", `<button type="button" role="tab" aria-selected="false" aria-controls="${id}-panel" id="${id}" tabindex="-1" > ${label} </button>`, );
panels.insertAdjacentHTML( "beforeend", `<section tabindex="0" role="tabpanel" aria-labelledby="${id}" id="${id}-panel" hidden > ${content} </section>`, );
tabsEl.init();});Example
Section titled “Example”Add tabs at runtime
The pattern is: destroy() → mutate DOM → init().
The galeb duhr is a curious boulder-like creature with appendages that act as hands and feet.
Trolls are horrid carnivores found in all climes, from arctic wastelands to tropical jungles.
Jermlaine are a diminutive humanoid race that dwells in tunnels and ambushes hapless adventurers.
Listening to tab-activate and tab-delete
Section titled “Listening to tab-activate and tab-delete”const tabsEl = document.querySelector("cinq-tabs");
tabsEl.querySelectorAll('[role="tab"]').forEach(tab => { tab.addEventListener("tab-activate", ({ detail }) => { const { controls, element } = detail; console.log("Activated tab:", controls, element); });
tab.addEventListener("tab-delete", ({ detail }) => { const { controls, element } = detail; console.log("Deleted tab:", controls, element); });});Interactive example
Section titled “Interactive example”Barracudas are swift marauders of warm salt waters.
A dracolich is an undead dragon created by forbidden rites.
A galeb duhr looks like a boulder until it moves.
Hash syncing
Section titled “Hash syncing”When data-tabs-hash="true", activating a tab updates location.hash with the tab id. Reloading the page with that hash will restore the active tab:
The galeb duhr looks like a boulder until it moves; its tab id
#hash-galeb is reflected in the URL when active.
Cloakers lurk like ordinary cloaks until they unfold; activating this tab sets
#hash-cloaker in the address bar.
Nested tabs
Section titled “Nested tabs”You can safely nest <cinq-tabs> as long as each tablist / panel group is scoped within its own component.
Inner tabs are fully independent: keyboard navigation and ARIA state are scoped to each
cinq-tabs instance.
The adherer looks like a mummy, with folds of off-white skin resembling filthy bandages.
Crimbils are malicious fey rumored to be a corrupted bloodline of wood gnomes.
Plush golems are every parent’s nightmare; they start as toys and quickly become full-fledged family members.
If the tablist inherits direction: rtl (e.g. dir="rtl"), left/right arrow navigation follows the reading direction.
<div dir="rtl"> <cinq-tabs data-tabs-hash="false">…</cinq-tabs></div>