What is it

A tabs component that provides different sections of content that are displayed one at a time when the user selects that information.

When to use it

The tabbed user interface enables users to jump to their target section quickly. Tabs present like logically group information on the same page. Information should

Users should not need to see content of multiple tabs simultaneously and the user should be able to easily recognise where they are within the content.

How to use it

The structure of the tab set is defined in html. There are two forms supported. Adding a class of .tab-group to the container element will work in place of the tabset tag, and the tab panels can be defined using either tab="" or data-tab="". Passing an optional element to the init function will initialise tabs within that element. Adding a order="" or data-order="" element to the tabset you can have the tabs sorted in a consistent order across tabsets.

Example

tabset(order="tab2, tab1")
	div(tab="tab1")
	div(tab="tab2")
        
          
tabset#uniqueID(order="tab title 2,tab title 1")
	div(tab="[tab title 1]")
	div(tab="[tab title 2]")
		
tabset, .tab-group {
	margin: 2rem 0 1rem 0;
}
tabset [role=tablist], .tab-group [role=tablist] {
	display: -webkit-box;
	display: -ms-flexbox;
	display: flex;
	margin: 0;
	padding: 0;
}
tabset [role=tablist] li.separator, .tab-group [role=tablist] li.separator {
	border-bottom: 1px solid #7f7f7f;
	display: inline-block;
	margin: 0.45rem 0 0 0;
	width: 100%;
}
tabset [role=tablist] li[role=tab], .tab-group [role=tablist] li[role=tab] {
	background-color: #FFF;
	border-left: 1px solid #7f7f7f;
	border-right: 1px solid #7f7f7f;
	border-radius: 0.5rem 0.5rem 0 0;
	border-top: 1px solid #7f7f7f;
	cursor: pointer;
	display: inline;
	margin: 0;
	padding: 1rem 1.5rem 0.14rem 1.5rem;
	z-index: 2;
}
tabset [role=tablist] li[role=tab]:last-of-type, .tab-group [role=tablist] li[role=tab]:last-of-type {
	border-right: 1px solid #7f7f7f;
}
tabset [role=tablist] li[role=tab]:not(.selected), .tab-group [role=tablist] li[role=tab]:not(.selected) {
	background-color: #f0f0f0;
	border-bottom: 1px solid #7f7f7f;
}
tabset [role=tablist] li[role=tab] span, .tab-group [role=tablist] li[role=tab] span {
	display: block;
	margin: 0 0 0.5rem 0;
}
tabset .tab-hidden, .tab-group .tab-hidden {
	display: none;
}
tabset [role=tabpanel], .tab-group [role=tabpanel] {
	background-color: #FFF;
	border: 1px solid #7f7f7f;
	border-top: none;
	padding: 1rem;
	z-index: 1;
}
tabset [role=tabpanel]:not(.open), .tab-group [role=tabpanel]:not(.open) {
	display: none;
}

Example

@use "scss/core/tabs/_tabs";
@include tabs{ 
	// optional content block
};
//		DS2 core (c) 2024 Alexander McIlwraith 
//		Licensed under CC BY-SA 4.0 

$tab-border: #7f7f7f !default; 
$tab-panel-background-color: #FFF !default;
$tab-panel-top-border: #7f7f7f !default;
$tab-panel-top-border-width: 1px !default;
$tab-selected: #FFF !default;
$tab-selected-text: #000 !default;
$tab-notselected: #f0f0f0 !default;
$tab-notselected-text: #000 !default;

@mixin tabs {
	tabset, .tab-group {
		margin: 2rem 0 1rem 0;

		[role="tablist"] {
			display: flex;
			margin: 0;
			padding: 0;


			li {
				&.separator {	
					border-bottom: 1px solid $tab-border;
					display: inline-block;
					margin: .45rem 0 0 0;
					width: 100%;
				}

				&[role="tab"] {
					background-color: $tab-selected;
					border-left: 1px solid $tab-border;
					border-right: 1px solid $tab-border;
					border-radius: .5rem .5rem 0 0;
					border-top: 1px solid $tab-border;
					cursor:pointer;
					display: inline;
					margin: 0;
					padding: 1rem 1.5rem .14rem 1.5rem;
					z-index: 2;
					
					&:last-of-type {
						border-right: 1px solid $tab-border;
					}

					&:not(.selected) {
						background-color: $tab-notselected;
						border-bottom: 1px solid $tab-border;
									/// color
					}

					span {
						display: block;
						margin: 0 0 .5rem 0;
					}
				}
			}
		}

		.tab-hidden {
			display: none;
		}
		
		[role="tabpanel"] {
			background-color: $tab-selected;
			border: 1px solid $tab-border;
			border-top: none;
			padding: 1rem;
			z-index: 1;

			&:not(.open) {
				display: none;
			}
		}
		@content;
	}
}

Example

import * as tabs from "./js/core/tabs/_tabs.js";
tabs.init();
/*  DS2 core (c) 2024 Alexander McIlwraith 
	Released under Creative Commons Attribution-ShareAlike 4.0 International
 */

// create a pure JS mouse click event
const click = new MouseEvent('click', {
	view: window,
	bubbles: false,
	cancelable: true
});

const waitForElement = (selector) => {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                observer.disconnect();
                resolve(document.querySelector(selector));
            }
        });

        // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

const chooseTab = (tab) => {
	const siblings = Array.from(tab.parentNode.children);
	siblings.forEach(sibling => sibling.classList.remove("selected"));
	tab.classList.add("selected");
	
	const tabPanels = Array.from(tab.parentNode.parentNode.children)
	tabPanels.forEach(panel => panel.classList.remove("open"));

	const tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel");
	document.getElementById(tabPanelId).classList.add("open");
}

let pushState = 0;
let tabsetCount = 0;

export function init(container = document, spacer = true, args = {}) {
	container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => {
		if (tabGroup.querySelector("[role=tablist]") === null) {
			if (tabGroup.getAttribute("id") == null) {
				tabGroup.setAttribute("id", "tab-group-" + tabsetCount);
				tabsetCount++;
			}

			const tabgroup = tabGroup.getAttribute("id");
			let tablist = "";

			Array.from(tabGroup.children).forEach(child => {

				// is details? 
				let dtls = child.nodeName == "DETAILS" ? true : false;

				// get the tab text 
				let tab = dtls ? child.querySelector("summary").innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");

				// if the tab text is not blank 
				if (tab !== null) {
					const tabID = tab.replace(/\W+/g, "-").toLowerCase();
					

					// define the tab panel content
					let tabPanel = null;
					if (dtls) { 
						tabPanel = child;
						tabPanel.setAttribute("open", "");
					} else {
						tabPanel = document.createElement('div');
						tabPanel.appendChild(child.cloneNode(true));
					}

					tabPanel.id = `tab-panel-${tabgroup}-${tabID}`;
					tabPanel.className = tablist === "" ? "open" : "";
					tabPanel.setAttribute("role", "tabpanel");
					tabPanel.setAttribute("tabindex", "0");
					tabPanel.setAttribute("aria-labelledby", `tab-${tabgroup}-${tabID}`);
					child.parentNode.replaceChild(tabPanel, child);
					tablist += ``;
				} else {
					child.classList.add("tab-hidden");
				}
			});

			const ul = document.createElement('ul');
			ul.setAttribute("role", "tablist");
			tabGroup.insertBefore(ul, tabGroup.firstChild);
			ul.innerHTML = spacer != true ? `${tablist}` : `${tablist}`;

			if ( tabGroup.hasAttribute("order") || tabGroup.hasAttribute("data-order") ) {
				let order = (tabGroup.getAttribute("order") || tabGroup.getAttribute("data-order")).split(",");

				const items = Array.from(ul.getElementsByTagName("li"));
				items.sort((a, b) => {
					console.log("here")
					const aa = order.indexOf(a.textContent.trim());
					const bb = order.indexOf(b.textContent.trim());

					// Check if both items exist in orderArray
					if (aa === -1) return 1; // Move to the end if not found
					if (bb === -1) return -1; // Move to the end if not found

					return aa - bb; // Sort based on the defined order
				});

				ul.innerHTML = '';
				items.forEach(item => ul.appendChild(item));
			}

			tabGroup.querySelectorAll('[role="tab"]').forEach(tab => {
				tab.addEventListener("click", (evt) => {
					if (pushState == 0) {
						window.history.pushState({rand: Math.random(), pushState: pushState, tab: tab.parentNode.firstChild.getAttribute("id")}, "", `#${tab.parentNode.firstChild.getAttribute("id")}`);
						pushState++;
					}

					chooseTab(evt.currentTarget);
					window.history.pushState({rand: Math.random(), pushState: pushState, tab: tab.getAttribute("id")}, "", `#${tab.getAttribute("id")}`);
					pushState++;
				});

				tab.addEventListener("keypress", (e) => {
					e.preventDefault();
					if( e.which == 32 || e.which == 13 ) {
						e.currentTarget.dispatchEvent(click);
					}
				})
			});
			ul.querySelector("li").classList.add("selected");
		}

		if (document.location.hash != "" && document.location.hash.substring(0,5) == "#tab-") {
			waitForElement(document.location.hash).then((el) => {
				//el.scrollIntoView();
				el.focus();
				el.dispatchEvent(click);
			});
		}
	});

	window.addEventListener("popstate", function (e) {
		e.preventDefault();
		if (e.state != null) {
			chooseTab(document.querySelector(`#${e.state.tab}`));
		} else {
			history.go(-1);
		}
	});
}