A tabs component that provides different sections of content that are displayed one at a time when the user selects that information.
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.
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.
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] li.selected, .tab-group [role=tablist] li.selected {
background-color: var(--colour-green);
color: var(--colour-white);
}
tabset > ul,
tabset .tab-scroll > ul, .tab-group > ul,
.tab-group .tab-scroll > ul {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
margin: 0;
padding: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
tabset > ul li.separator,
tabset .tab-scroll > ul li.separator, .tab-group > ul li.separator,
.tab-group .tab-scroll > ul li.separator {
border-bottom: 1px solid #7f7f7f;
border-left: 1px solid #7f7f7f;
display: inline-block;
-webkit-box-flex: 1;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
margin: 0.45rem 0 0 0;
width: auto;
}
tabset .tab-hidden, .tab-group .tab-hidden {
display: none;
}
tabset [role=tab], .tab-group [role=tab] {
background-color: #FFF;
border-left: 1px solid #7f7f7f;
border-radius: 0.5rem 0.5rem 0 0;
border-top: 1px solid #7f7f7f;
cursor: pointer;
display: block;
-webkit-box-flex: 0;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
margin: 0;
max-width: 100vw;
overflow: hidden;
padding: 1rem 1.5rem 0.14rem 1.5rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
white-space: normal;
word-break: break-word;
}
tabset [role=tab]:last-of-type, .tab-group [role=tab]:last-of-type {
border-right: 1px solid #7f7f7f;
}
tabset [role=tab]:not(.selected), .tab-group [role=tab]:not(.selected) {
background-color: #f0f0f0;
border-bottom: 1px solid #7f7f7f;
}
tabset [role=tab] span, .tab-group [role=tab] span {
display: block;
margin: 0 0 0.5rem 0;
}
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;
}
.tab-scroll {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tab-scroll::-webkit-scrollbar {
display: none;
}
.tab-scroll > ul {
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
min-width: 100%;
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
}
.tab-scroll-wrapper {
position: relative;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tab-scroll-wrapper::before, .tab-scroll-wrapper::after {
content: "";
position: absolute;
top: 0;
width: 2.5rem;
height: 100%;
pointer-events: none;
z-index: 2;
opacity: 0;
-webkit-transition: opacity 0.2s ease;
transition: opacity 0.2s ease;
}
.tab-scroll-wrapper::before {
background: radial-gradient(circle at left center, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0.5) 75%, rgba(255, 255, 255, 0.25) 95%, transparent 100%);
border-top-right-radius: 2.5rem;
border-bottom-right-radius: 2.5rem;
left: 0;
}
.tab-scroll-wrapper::after {
right: 0;
background: radial-gradient(circle at right center, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.9) 50%, rgba(255, 255, 255, 0.5) 75%, rgba(255, 255, 255, 0.25) 95%, transparent 100%);
border-top-left-radius: 2.5rem;
border-bottom-left-radius: 2.5rem;
}
.tab-scroll-wrapper.is-overflowing::before, .tab-scroll-wrapper.is-overflowing::after {
opacity: 0;
-webkit-transition: opacity 0.15s ease-out;
transition: opacity 0.15s ease-out;
-webkit-transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
}
.tab-scroll-wrapper.is-overflowing.show-left::before {
opacity: 1;
}
.tab-scroll-wrapper.is-overflowing.show-right::after {
opacity: 1;
}
.tab-scroll-wrapper .tab-scroll-arrow {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
cursor: pointer;
display: none;
height: 100%;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
position: absolute;
top: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
width: 2.5rem;
z-index: 3;
}
.tab-scroll-wrapper .tab-scroll-arrow path {
fill: #7f7f7f;
}
.tab-scroll-wrapper .tab-scroll-arrow.left {
left: 0;
}
.tab-scroll-wrapper .tab-scroll-arrow.right {
right: 0;
}
.tab-scroll-wrapper .tab-scroll-arrow.hidden {
display: none !important;
pointer-events: none;
visibility: hidden;
}
.tab-scroll-wrapper.is-overflowing .tab-scroll-arrow {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
@use "scss/core/tabs/_tabs";
@include tabs{
// optional content block
};
$tab-border: #7f7f7f !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"] li.selected {
background-color: var(--colour-green);
color: var(--colour-white);
}
> ul,
.tab-scroll > ul {
display: flex;
margin: 0;
padding: 0;
user-select: none;
li.separator {
border-bottom: 1px solid $tab-border;
border-left: 1px solid $tab-border;
display: inline-block;
flex: 1 1 auto;
margin: .45rem 0 0 0;
width: auto;
}
}
.tab-hidden {
display: none;
}
[role="tab"] {
background-color: $tab-selected;
border-left: 1px solid $tab-border;
border-radius: .5rem .5rem 0 0;
border-top: 1px solid $tab-border;
cursor: pointer;
display: block;
flex: 0 0 auto;
margin: 0;
max-width: 100vw;
overflow: hidden;
padding: 1rem 1.5rem .14rem 1.5rem;
user-select: none;
white-space: normal;
word-break: break-word;
&:last-of-type {
border-right: 1px solid $tab-border;
}
&:not(.selected) {
background-color: $tab-notselected;
border-bottom: 1px solid $tab-border;
}
span {
display: block;
margin: 0 0 .5rem 0;
}
}
[role="tabpanel"] {
background-color: $tab-selected;
border: 1px solid $tab-border;
border-top: none;
padding: 1rem;
z-index: 1;
&:not(.open) {
display: none;
}
@content;
}
}
// scroller
.tab-scroll {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
user-select: none;
&::-webkit-scrollbar {
display: none;
}
> ul {
flex-wrap: nowrap;
min-width: 100%;
width: max-content;
}
}
// wrapper (now owns the fade)
.tab-scroll-wrapper {
position: relative;
user-select: none;
$radial: 2.5rem;
// edge fades (this is the fix)
&::before,
&::after {
content: "";
position: absolute;
top: 0;
width: 2.5rem;
height: 100%;
pointer-events: none;
z-index: 2;
opacity: 0; // hidden by default
transition: opacity 0.2s ease;
}
// left fade
&::before {
background: radial-gradient(
circle at left center,
rgba(255,255,255,1) 0%,
rgba(255,255,255,0.9) 50%,
rgba(255,255,255,0.5) 75%,
rgba(255,255,255,0.25) 95%,
transparent 100%
);
border-top-right-radius: $radial;
border-bottom-right-radius: $radial;
left: 0;
}
// right fade
&::after {
right: 0;
background: radial-gradient(
circle at right center,
rgba(255,255,255,1) 0%,
rgba(255,255,255,0.9) 50%,
rgba(255,255,255,0.5) 75%,
rgba(255,255,255,0.25) 95%,
transparent 100%
);
border-top-left-radius: $radial;
border-bottom-left-radius: $radial;
}
// show fade only when overflowing
&.is-overflowing {
&::before,
&::after {
opacity: 0;
transition: opacity 0.15s ease-out;
transition: opacity 0.12s cubic-bezier(0.4, 0, 0.2, 1);
}
&.show-left {
&::before {
opacity: 1;
}
}
&.show-right {
&::after {
opacity: 1;
}
}
}
// arrows
.tab-scroll-arrow {
align-items: center;
cursor: pointer;
display: none;
height: 100%;
justify-content: center;
position: absolute;
top: 0;
user-select: none;
width: 2.5rem;
z-index: 3;
path {
fill: $tab-border;
}
&.left {
left: 0;
}
&.right {
right: 0;
}
&.hidden {
display: none !important;
pointer-events: none;
visibility: hidden;
}
}
// only show arrows when overflow
&.is-overflowing {
.tab-scroll-arrow {
display: flex;
}
}
}
}
import * as tabs from "./js/core/tabs/_tabs.js"; tabs.init();
/* DS2 core (c) 2024-2026 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
});
// wait for an element to appear in the DOM
const waitForElement = (selector) => {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
observer.disconnect();
resolve(document.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
};
// Tab logic
const tabNamespace = "#tab:";
const chooseTab = (tab) => {
const siblings = Array.from(
tab.closest('[role="tablist"]').querySelectorAll('[role="tab"]')
);
siblings.forEach(sibling => sibling.classList.remove("selected"));
tab.classList.add("selected");
const tabGroup = tab.closest("tabset, .tab-group");
const tabPanels = Array.from(
tabGroup.querySelectorAll('[role="tabpanel"]')
);
tabPanels.forEach(panel => panel.classList.remove("open"));
const tabPanelId = tab.getAttribute("id").replace("tab", "tab-panel");
document.getElementById(tabPanelId).classList.add("open");
};
let pushStateCount = 0;
let tabsetCount = 0;
export function init(container = document, spacer = true) {
container.querySelectorAll(".tab-group, tabset").forEach(tabGroup => {
if (tabGroup.querySelector('[role="tablist"]') === null) {
if (tabGroup.getAttribute("id") == null) {
tabGroup.setAttribute("id", `tab-group-${tabsetCount++}`);
}
const tabgroup = tabGroup.getAttribute("id");
let tablist = "";
Array.from(tabGroup.children).forEach(child => {
const isDetails = child.nodeName === "DETAILS";
const tabLabel = isDetails ? child.querySelector("summary")?.innerHTML : child.getAttribute("tab") || child.getAttribute("data-tab");
if (tabLabel !== null) {
const tabID = tabLabel.replace(/\W+/g, "-").toLowerCase();
let tabPanel;
if (isDetails) {
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 += `