TabList v2.0.0
Organize content into separate sections, allowing user to switch between them by selecting different tabs.
Habitant morbi
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
Vestibulum Tortor Quam
Feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat.
Malesuada Fames
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Note: By default, tabs display inline on both mobile and desktop, wrapping as needed based on screen size.
Reminder: Do not confuse a tabbed interface with a horizontal accordion—they are not the same. The key difference lies in their behavior. If you expect content sections to always close when a tab button is pressed, then please use the Accordion component. However, a tabbed UI should have only a few tabs. If there are too many, then you should consider a horizontal accordion instead.
Tab Placement: Respect the Natural Flow Anchor (Tab Placement: Respect the Natural Flow)
Placing tab controls below the content they manage creates a disconnect between the control and its content, resulting in a confusing and inaccessible experience across keyboard, mobile, and magnified views.
This pattern creates multiple usability and accessibility issues:
- Breaks expected keyboard flow: Users navigate left-to-right, top-to-bottom. When tabs are at the bottom, focus moves away from the active panel instead of into it, forcing users to tab backwards which is not predictable and violates WCAG 3.2.3 Consistent Navigation, WCAG 2.4.3 Focus Order
- Poor mobile usability: Content updates occur outside the user’s current view, making changes easy to miss.
- Screen magnification barriers: Magnified users are likely focused on the tabs; updated content appears above and may not be visible, and users may scroll the wrong direction.
- Focus and spatial disorientation: If panel height changes, activating a tab can shift the tabs’ position, causing users to lose their place.
- Violates established tab design conventions: Tabs are expected to precede and control the content that follows.
Example
<div class="tablist">
<ul class="tablist__list">
<li><a class="tablist__tab" href="#default-setup-1">Habitant Morbi</a></li>
<li><a class="tablist__tab" href="#default-setup-2">Vestibulum Tortor Quam</a></li>
<li><a class="tablist__tab" href="#default-setup-3">Malesuada Fames</a></li>
</ul>
<div class="tablist__panel" id="default-setup-1">
<div class="tablist__content">
<h2>Habitant morbi</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.</p>
<p>Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.</p>
</div>
</div>
<div class="tablist__panel" id="default-setup-2">
<div class="tablist__content">
<h2>Vestibulum Tortor Quam</h2>
<p>Feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.</p>
<p>Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat.</p>
</div>
</div>
<div class="tablist__panel" id="default-setup-3">
<div class="tablist__content">
<h2>Malesuada Fames</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
</div>/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
Dependencies: Sass
*/
.tablist {
// Component Variables
--tablist-color-active: #ffffff; // Active State
--tablist-color-default: #d3d3d3; // Default States
--tablist-color-interactive: #a9a9a9; // Interactive State
--tablist-color-selected: #6F00EF; // Selected States
// Breakpoints (CSS variables are not allowed in media queries - boo!)
$tablist-large: 55em; // Large Viewport
$tablist-medium: 48.625em; // Medium Viewport
color: var(--tablist-color-active);
position: relative;
&:has([aria-orientation]) {
@media(min-width: $tablist-large) {
display: grid;
grid-template-columns: calc(280rem /16) auto;
}
}
// Tablist Container (Optional)
&__container {
}
// Tablist Heading (Optional)
&__heading {
color: var(--tablist-color-selected);
font-size: 1.9rem;
text-align: center;
width: auto;
}
&__container, &__list {
align-self: center;
}
&__list {
display: flex;
flex-wrap: wrap;
gap: calc(4rem/16);
list-style: none;
margin: 0;
padding: 0;
&[aria-orientation] {
@media(min-width: $tablist-large) {
display: block;
}
}
[data-stack] & {
@media(max-width: $tablist-medium) {
display: block;
}
}
[data-scroll] & {
// This max-width can be changed depending on amount of list items you have.
@media(max-width: $tablist-medium) {
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: inline mandatory;
white-space: nowrap;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
&::-webkit-scrollbar {
height: calc(20rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(8rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
}
}
}
}
li {
flex-grow: 1;
scroll-snap-align: start;
@media(min-width: $tablist-medium) {
flex-grow: 0;
}
}
}
// Tab Button
&__tab {
background-color: var(--tablist-color-default);
color: #000;
display: block;
padding: 1em;
text-align: center;
text-decoration: none;
[data-animation] & {
@media (prefers-reduced-motion: no-preference) {
transition: 600ms;
}
}
&:hover {
background-color: var(--tablist-color-interactive);
}
&[aria-selected="true"] {
background-color: var(--tablist-color-selected);
color: var(--tablist-color-active);
}
}
// Tab Panel
&__panel {
background-color: var(--tablist-color-selected);
overflow: hidden;
padding: 1em;
position: relative;
[data-fixed-height] & {
max-height: calc(400em/16);
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
padding-inline-end: 0.5em;
&::-webkit-scrollbar {
width: calc(18rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
margin-block: 0.5em;
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(6rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
min-height: calc(100rem / 16);
}
}
}
}
// Tab Content
&__content {
position: relative;
// Your specific styles, if any.
[data-animation] & {
:not([hidden]) & {
inset-block-start: 0;
opacity: 1;
@starting-style {
inset-block-start: 3em;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transition: inset 600ms, opacity 300ms;
}
}
}
}
// Focus State
// Accessibility: Never remove focus outlines! Either redesign the outline or design an alternative focus state.
// However, a 3:1 ratio will be required. Learn: https://webaim.org/articles/contrast/
&__tab, &__panel {
&:focus {
outline-offset: -6px;
}
}
}/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
*/
(() => {
"use strict";
const initTablist = () => {
// Classes, data attributes, states, and strings.
const tabListClass = ".tablist";
const tabListListClass = ".tablist__list";
const tabListClassName = "tablist__tab";
const tabListTabClass = `.${tabListClassName}`;
const tabListDataDisableAnchor = "data-disable-anchor";
const tabListDataVertical = "data-vertical";
const tabListPanelClass = ".tablist__panel";
const tabLists = document.querySelectorAll(tabListClass);
const URLFragment = location.hash.slice(1);
// Display which version is in use via console:
console.log("%cTabList%cv2.0.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");
// Set up all Tablists on page
tabLists.forEach((list, e) => {
const index = e + 1;
list.setAttribute("id", `tablist-${index}`);
const listContainer = list.querySelector(tabListListClass);
listContainer.setAttribute("role", "tablist");
if (list.hasAttribute(tabListDataVertical)) {
listContainer.setAttribute("aria-orientation", "vertical");
}
// Get all tabs within tablist.
const tabs = list.querySelectorAll(tabListTabClass);
// Get all tabpanels within tablist.
const panels = list.querySelectorAll(tabListPanelClass);
// Array used to hold all panel IDs.
const panelNames = [];
// Prep each tab.
tabs.forEach((tab) => {
const tabID = tab.getAttribute("href").replace("#", "");
// Push all panel ID's to array.
panelNames.push(tabID);
// Setup each tab.
tab.parentElement.setAttribute("role", "presentation");
tab.setAttribute("aria-controls", tabID);
tab.setAttribute("id", `tab-${tabID}`);
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
// Highlight tab based on hash.
if (tabID === URLFragment) {
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
}
tab.addEventListener("click", (e) => {
const panelID = tab.getAttribute("href").replace("#", "");
tabs.forEach((tab) => {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide any open panels
const panels = tab.closest(tabListClass).querySelectorAll(tabListPanelClass);
const panelTarget = tab.closest(tabListClass).querySelector(`#${panelID}`);
panels.forEach((panel) => {
panel.setAttribute("hidden", "");
panel.removeAttribute("tabindex");
});
// Show selected panel
panelTarget.removeAttribute("hidden");
panelTarget.setAttribute("tabindex", "0");
// Highlight selected tab
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
if (!tab.closest(tabListClass).hasAttribute(tabListDataDisableAnchor)) {
history.pushState(null, null, `#${tabID}`);
}
e.preventDefault();
});
// Get tab links used in tablist.
const tabLinks = list.querySelector(tabListListClass).querySelectorAll(tabListTabClass);
tab.addEventListener("keydown", (e) => {
// Check if the key pressed is an arrow key or Home/End
if (["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft", "Home", "End"].includes(e.key)) {
e.preventDefault(); // Prevent default scrolling behavior when arrows keys clicked.
// Get the index of the currently focused link
let focusedIndex = Array.from(tabLinks).indexOf(document.activeElement);
// Calculate the new index based on the arrow key pressed
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
focusedIndex = (focusedIndex - 1 + tabLinks.length) % tabLinks.length; // Move left
} else if (["ArrowRight", "ArrowDown"].includes(e.key)) {
focusedIndex = (focusedIndex + 1) % tabLinks.length; // Move right
} else if (e.key === "Home") {
focusedIndex = 0; // Move to first tab
} else if (e.key === "End") {
focusedIndex = tabLinks.length - 1; // Move to last tab
}
// Focus and trigger event
tabLinks[focusedIndex].focus();
tabLinks[focusedIndex].click();
}
});
});
panels.forEach((panel) => {
const panelID = panel.getAttribute("id");
panel.setAttribute("aria-labelledby", `tab-${panelID}`);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("tabindex", "0");
panel.setAttribute("hidden", "");
if (panelID === URLFragment) {
panel.removeAttribute("hidden");
}
});
if (!URLFragment || !panelNames.includes(URLFragment)) {
tabs[0].setAttribute("aria-selected", "true");
tabs[0].removeAttribute("tabindex");
panels[0].removeAttribute("hidden");
panels[0].setAttribute("tabindex", "0");
}
});
// All TabList panels must have unique IDs. Check to see if duplicate ID's exist and alert developer.
// Note: This function may not be needed. Instead, simply having functionality so dependent on ID, may make this moot.
const allPanelID = [];
const tabPanels = document.querySelectorAll(tabListPanelClass);
tabPanels.forEach((panel) => {
const panelID = panel.id;
allPanelID.push(panelID);
});
allPanelID.forEach((element, index, array) => {
// Check if the current element is equal to any subsequent element
array.slice(index + 1).forEach((nextElement) => {
if (element === nextElement) {
console.log(`%c Warning: Duplicate Tablist ID found: #${element}. Tab panels must only contain unique ID values. `, "background: #ff0000; color: #fff");
}
});
});
}
initTablist();
})();Habitant morbi
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
Vestibulum Tortor Quam
Feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat.
Malesuada Fames
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Reminder: Do not confuse a tabbed interface with a horizontal accordion—they are not the same. The key difference lies in their behavior. If you expect content sections to always close when a tab button is pressed, then please use the Accordion component. However, a tabbed UI should have only a few tabs. If there are too many, then you should consider a horizontal accordion instead.
Experimental: Please note that this example uses the new @starting-style at-rule and transition-behavior: allow-discrete; to produce animation. This technique is supported in most major browsers. If you require specific animations in non-supporting browserss, you will need to come up with a traditional implementation.
Tab Placement: Respect the Natural Flow Anchor (Tab Placement: Respect the Natural Flow)
Placing tab controls below the content they manage creates a disconnect between the control and its content, resulting in a confusing and inaccessible experience across keyboard, mobile, and magnified views.
This pattern creates multiple usability and accessibility issues:
- Breaks expected keyboard flow: Users navigate left-to-right, top-to-bottom. When tabs are at the bottom, focus moves away from the active panel instead of into it, forcing users to tab backwards which is not predictable and violates WCAG 3.2.3 Consistent Navigation, WCAG 2.4.3 Focus Order
- Poor mobile usability: Content updates occur outside the user’s current view, making changes easy to miss.
- Screen magnification barriers: Magnified users are likely focused on the tabs; updated content appears above and may not be visible, and users may scroll the wrong direction.
- Focus and spatial disorientation: If panel height changes, activating a tab can shift the tabs’ position, causing users to lose their place.
- Violates established tab design conventions: Tabs are expected to precede and control the content that follows.
Example
<div class="tablist" data-animation>
<ul class="tablist__list">
<li><a class="tablist__tab" href="#animation-1">Habitant Morbi</a></li>
<li><a class="tablist__tab" href="#animation-2">Vestibulum Tortor Quam</a></li>
<li><a class="tablist__tab" href="#animation-3">Malesuada Fames</a></li>
</ul>
<div class="tablist__panel" id="animation-1">
<div class="tablist__content">
<h2>Habitant morbi</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.</p>
<p>Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.</p>
</div>
</div>
<div class="tablist__panel" id="animation-2">
<div class="tablist__content">
<h2>Vestibulum Tortor Quam</h2>
<p>Feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.</p>
<p>Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat.</p>
</div>
</div>
<div class="tablist__panel" id="animation-3">
<div class="tablist__content">
<h2>Malesuada Fames</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
</div>/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
Dependencies: Sass
*/
.tablist {
// Component Variables
--tablist-color-active: #ffffff; // Active State
--tablist-color-default: #d3d3d3; // Default States
--tablist-color-interactive: #a9a9a9; // Interactive State
--tablist-color-selected: #6F00EF; // Selected States
// Breakpoints (CSS variables are not allowed in media queries - boo!)
$tablist-large: 55em; // Large Viewport
$tablist-medium: 48.625em; // Medium Viewport
color: var(--tablist-color-active);
position: relative;
&:has([aria-orientation]) {
@media(min-width: $tablist-large) {
display: grid;
grid-template-columns: calc(280rem /16) auto;
}
}
// Tablist Container (Optional)
&__container {
}
// Tablist Heading (Optional)
&__heading {
color: var(--tablist-color-selected);
font-size: 1.9rem;
text-align: center;
width: auto;
}
&__container, &__list {
align-self: center;
}
&__list {
display: flex;
flex-wrap: wrap;
gap: calc(4rem/16);
list-style: none;
margin: 0;
padding: 0;
&[aria-orientation] {
@media(min-width: $tablist-large) {
display: block;
}
}
[data-stack] & {
@media(max-width: $tablist-medium) {
display: block;
}
}
[data-scroll] & {
// This max-width can be changed depending on amount of list items you have.
@media(max-width: $tablist-medium) {
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: inline mandatory;
white-space: nowrap;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
&::-webkit-scrollbar {
height: calc(20rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(8rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
}
}
}
}
li {
flex-grow: 1;
scroll-snap-align: start;
@media(min-width: $tablist-medium) {
flex-grow: 0;
}
}
}
// Tab Button
&__tab {
background-color: var(--tablist-color-default);
color: #000;
display: block;
padding: 1em;
text-align: center;
text-decoration: none;
[data-animation] & {
@media (prefers-reduced-motion: no-preference) {
transition: 600ms;
}
}
&:hover {
background-color: var(--tablist-color-interactive);
}
&[aria-selected="true"] {
background-color: var(--tablist-color-selected);
color: var(--tablist-color-active);
}
}
// Tab Panel
&__panel {
background-color: var(--tablist-color-selected);
overflow: hidden;
padding: 1em;
position: relative;
[data-fixed-height] & {
max-height: calc(400em/16);
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
padding-inline-end: 0.5em;
&::-webkit-scrollbar {
width: calc(18rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
margin-block: 0.5em;
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(6rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
min-height: calc(100rem / 16);
}
}
}
}
// Tab Content
&__content {
position: relative;
// Your specific styles, if any.
[data-animation] & {
:not([hidden]) & {
inset-block-start: 0;
opacity: 1;
@starting-style {
inset-block-start: 3em;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transition: inset 600ms, opacity 300ms;
}
}
}
}
// Focus State
// Accessibility: Never remove focus outlines! Either redesign the outline or design an alternative focus state.
// However, a 3:1 ratio will be required. Learn: https://webaim.org/articles/contrast/
&__tab, &__panel {
&:focus {
outline-offset: -6px;
}
}
}/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
*/
(() => {
"use strict";
const initTablist = () => {
// Classes, data attributes, states, and strings.
const tabListClass = ".tablist";
const tabListListClass = ".tablist__list";
const tabListClassName = "tablist__tab";
const tabListTabClass = `.${tabListClassName}`;
const tabListDataDisableAnchor = "data-disable-anchor";
const tabListDataVertical = "data-vertical";
const tabListPanelClass = ".tablist__panel";
const tabLists = document.querySelectorAll(tabListClass);
const URLFragment = location.hash.slice(1);
// Display which version is in use via console:
console.log("%cTabList%cv2.0.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");
// Set up all Tablists on page
tabLists.forEach((list, e) => {
const index = e + 1;
list.setAttribute("id", `tablist-${index}`);
const listContainer = list.querySelector(tabListListClass);
listContainer.setAttribute("role", "tablist");
if (list.hasAttribute(tabListDataVertical)) {
listContainer.setAttribute("aria-orientation", "vertical");
}
// Get all tabs within tablist.
const tabs = list.querySelectorAll(tabListTabClass);
// Get all tabpanels within tablist.
const panels = list.querySelectorAll(tabListPanelClass);
// Array used to hold all panel IDs.
const panelNames = [];
// Prep each tab.
tabs.forEach((tab) => {
const tabID = tab.getAttribute("href").replace("#", "");
// Push all panel ID's to array.
panelNames.push(tabID);
// Setup each tab.
tab.parentElement.setAttribute("role", "presentation");
tab.setAttribute("aria-controls", tabID);
tab.setAttribute("id", `tab-${tabID}`);
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
// Highlight tab based on hash.
if (tabID === URLFragment) {
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
}
tab.addEventListener("click", (e) => {
const panelID = tab.getAttribute("href").replace("#", "");
tabs.forEach((tab) => {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide any open panels
const panels = tab.closest(tabListClass).querySelectorAll(tabListPanelClass);
const panelTarget = tab.closest(tabListClass).querySelector(`#${panelID}`);
panels.forEach((panel) => {
panel.setAttribute("hidden", "");
panel.removeAttribute("tabindex");
});
// Show selected panel
panelTarget.removeAttribute("hidden");
panelTarget.setAttribute("tabindex", "0");
// Highlight selected tab
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
if (!tab.closest(tabListClass).hasAttribute(tabListDataDisableAnchor)) {
history.pushState(null, null, `#${tabID}`);
}
e.preventDefault();
});
// Get tab links used in tablist.
const tabLinks = list.querySelector(tabListListClass).querySelectorAll(tabListTabClass);
tab.addEventListener("keydown", (e) => {
// Check if the key pressed is an arrow key or Home/End
if (["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft", "Home", "End"].includes(e.key)) {
e.preventDefault(); // Prevent default scrolling behavior when arrows keys clicked.
// Get the index of the currently focused link
let focusedIndex = Array.from(tabLinks).indexOf(document.activeElement);
// Calculate the new index based on the arrow key pressed
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
focusedIndex = (focusedIndex - 1 + tabLinks.length) % tabLinks.length; // Move left
} else if (["ArrowRight", "ArrowDown"].includes(e.key)) {
focusedIndex = (focusedIndex + 1) % tabLinks.length; // Move right
} else if (e.key === "Home") {
focusedIndex = 0; // Move to first tab
} else if (e.key === "End") {
focusedIndex = tabLinks.length - 1; // Move to last tab
}
// Focus and trigger event
tabLinks[focusedIndex].focus();
tabLinks[focusedIndex].click();
}
});
});
panels.forEach((panel) => {
const panelID = panel.getAttribute("id");
panel.setAttribute("aria-labelledby", `tab-${panelID}`);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("tabindex", "0");
panel.setAttribute("hidden", "");
if (panelID === URLFragment) {
panel.removeAttribute("hidden");
}
});
if (!URLFragment || !panelNames.includes(URLFragment)) {
tabs[0].setAttribute("aria-selected", "true");
tabs[0].removeAttribute("tabindex");
panels[0].removeAttribute("hidden");
panels[0].setAttribute("tabindex", "0");
}
});
// All TabList panels must have unique IDs. Check to see if duplicate ID's exist and alert developer.
// Note: This function may not be needed. Instead, simply having functionality so dependent on ID, may make this moot.
const allPanelID = [];
const tabPanels = document.querySelectorAll(tabListPanelClass);
tabPanels.forEach((panel) => {
const panelID = panel.id;
allPanelID.push(panelID);
});
allPanelID.forEach((element, index, array) => {
// Check if the current element is equal to any subsequent element
array.slice(index + 1).forEach((nextElement) => {
if (element === nextElement) {
console.log(`%c Warning: Duplicate Tablist ID found: #${element}. Tab panels must only contain unique ID values. `, "background: #ff0000; color: #fff");
}
});
});
}
initTablist();
})();Habitant Morbi
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Vestibulum Tortor Quam
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Malesuada Fames
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Note: By default, clicking a tab adds an anchor to the URL, but this can be disabled if needed. However, anchors are useful for sharing specific content and tracking, so they should only be removed with a valid reason.
Reminder: Do not confuse a tabbed interface with a horizontal accordion—they are not the same. The key difference lies in their behavior. If you expect content sections to always close when a tab button is pressed, then please use the Accordion component. However, a tabbed UI should have only a few tabs. If there are too many, then you should consider a horizontal accordion instead.
Tab Placement: Respect the Natural Flow Anchor (Tab Placement: Respect the Natural Flow)
Placing tab controls below the content they manage creates a disconnect between the control and its content, resulting in a confusing and inaccessible experience across keyboard, mobile, and magnified views.
This pattern creates multiple usability and accessibility issues:
- Breaks expected keyboard flow: Users navigate left-to-right, top-to-bottom. When tabs are at the bottom, focus moves away from the active panel instead of into it, forcing users to tab backwards which is not predictable and violates WCAG 3.2.3 Consistent Navigation, WCAG 2.4.3 Focus Order
- Poor mobile usability: Content updates occur outside the user’s current view, making changes easy to miss.
- Screen magnification barriers: Magnified users are likely focused on the tabs; updated content appears above and may not be visible, and users may scroll the wrong direction.
- Focus and spatial disorientation: If panel height changes, activating a tab can shift the tabs’ position, causing users to lose their place.
- Violates established tab design conventions: Tabs are expected to precede and control the content that follows.
Example
<div class="tablist" data-disable-anchor>
<ul class="tablist__list">
<li><a class="tablist__tab" href="#disable-anchor-1">Habitant Morbi</a></li>
<li><a class="tablist__tab" href="#disable-anchor-2">Vestibulum Tortor Quam</a></li>
<li><a class="tablist__tab" href="#disable-anchor-3">Malesuada Fames</a></li>
</ul>
<div class="tablist__panel" id="disable-anchor-1">
<div class="tablist__content">
<h2>Habitant Morbi</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="disable-anchor-2">
<div class="tablist__content">
<h2>Vestibulum Tortor Quam</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="disable-anchor-3">
<div class="tablist__content">
<h2>Malesuada Fames</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
</div>/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
Dependencies: Sass
*/
.tablist {
// Component Variables
--tablist-color-active: #ffffff; // Active State
--tablist-color-default: #d3d3d3; // Default States
--tablist-color-interactive: #a9a9a9; // Interactive State
--tablist-color-selected: #6F00EF; // Selected States
// Breakpoints (CSS variables are not allowed in media queries - boo!)
$tablist-large: 55em; // Large Viewport
$tablist-medium: 48.625em; // Medium Viewport
color: var(--tablist-color-active);
position: relative;
&:has([aria-orientation]) {
@media(min-width: $tablist-large) {
display: grid;
grid-template-columns: calc(280rem /16) auto;
}
}
// Tablist Container (Optional)
&__container {
}
// Tablist Heading (Optional)
&__heading {
color: var(--tablist-color-selected);
font-size: 1.9rem;
text-align: center;
width: auto;
}
&__container, &__list {
align-self: center;
}
&__list {
display: flex;
flex-wrap: wrap;
gap: calc(4rem/16);
list-style: none;
margin: 0;
padding: 0;
&[aria-orientation] {
@media(min-width: $tablist-large) {
display: block;
}
}
[data-stack] & {
@media(max-width: $tablist-medium) {
display: block;
}
}
[data-scroll] & {
// This max-width can be changed depending on amount of list items you have.
@media(max-width: $tablist-medium) {
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: inline mandatory;
white-space: nowrap;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
&::-webkit-scrollbar {
height: calc(20rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(8rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
}
}
}
}
li {
flex-grow: 1;
scroll-snap-align: start;
@media(min-width: $tablist-medium) {
flex-grow: 0;
}
}
}
// Tab Button
&__tab {
background-color: var(--tablist-color-default);
color: #000;
display: block;
padding: 1em;
text-align: center;
text-decoration: none;
[data-animation] & {
@media (prefers-reduced-motion: no-preference) {
transition: 600ms;
}
}
&:hover {
background-color: var(--tablist-color-interactive);
}
&[aria-selected="true"] {
background-color: var(--tablist-color-selected);
color: var(--tablist-color-active);
}
}
// Tab Panel
&__panel {
background-color: var(--tablist-color-selected);
overflow: hidden;
padding: 1em;
position: relative;
[data-fixed-height] & {
max-height: calc(400em/16);
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
padding-inline-end: 0.5em;
&::-webkit-scrollbar {
width: calc(18rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
margin-block: 0.5em;
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(6rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
min-height: calc(100rem / 16);
}
}
}
}
// Tab Content
&__content {
position: relative;
// Your specific styles, if any.
[data-animation] & {
:not([hidden]) & {
inset-block-start: 0;
opacity: 1;
@starting-style {
inset-block-start: 3em;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transition: inset 600ms, opacity 300ms;
}
}
}
}
// Focus State
// Accessibility: Never remove focus outlines! Either redesign the outline or design an alternative focus state.
// However, a 3:1 ratio will be required. Learn: https://webaim.org/articles/contrast/
&__tab, &__panel {
&:focus {
outline-offset: -6px;
}
}
}/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
*/
(() => {
"use strict";
const initTablist = () => {
// Classes, data attributes, states, and strings.
const tabListClass = ".tablist";
const tabListListClass = ".tablist__list";
const tabListClassName = "tablist__tab";
const tabListTabClass = `.${tabListClassName}`;
const tabListDataDisableAnchor = "data-disable-anchor";
const tabListDataVertical = "data-vertical";
const tabListPanelClass = ".tablist__panel";
const tabLists = document.querySelectorAll(tabListClass);
const URLFragment = location.hash.slice(1);
// Display which version is in use via console:
console.log("%cTabList%cv2.0.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");
// Set up all Tablists on page
tabLists.forEach((list, e) => {
const index = e + 1;
list.setAttribute("id", `tablist-${index}`);
const listContainer = list.querySelector(tabListListClass);
listContainer.setAttribute("role", "tablist");
if (list.hasAttribute(tabListDataVertical)) {
listContainer.setAttribute("aria-orientation", "vertical");
}
// Get all tabs within tablist.
const tabs = list.querySelectorAll(tabListTabClass);
// Get all tabpanels within tablist.
const panels = list.querySelectorAll(tabListPanelClass);
// Array used to hold all panel IDs.
const panelNames = [];
// Prep each tab.
tabs.forEach((tab) => {
const tabID = tab.getAttribute("href").replace("#", "");
// Push all panel ID's to array.
panelNames.push(tabID);
// Setup each tab.
tab.parentElement.setAttribute("role", "presentation");
tab.setAttribute("aria-controls", tabID);
tab.setAttribute("id", `tab-${tabID}`);
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
// Highlight tab based on hash.
if (tabID === URLFragment) {
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
}
tab.addEventListener("click", (e) => {
const panelID = tab.getAttribute("href").replace("#", "");
tabs.forEach((tab) => {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide any open panels
const panels = tab.closest(tabListClass).querySelectorAll(tabListPanelClass);
const panelTarget = tab.closest(tabListClass).querySelector(`#${panelID}`);
panels.forEach((panel) => {
panel.setAttribute("hidden", "");
panel.removeAttribute("tabindex");
});
// Show selected panel
panelTarget.removeAttribute("hidden");
panelTarget.setAttribute("tabindex", "0");
// Highlight selected tab
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
if (!tab.closest(tabListClass).hasAttribute(tabListDataDisableAnchor)) {
history.pushState(null, null, `#${tabID}`);
}
e.preventDefault();
});
// Get tab links used in tablist.
const tabLinks = list.querySelector(tabListListClass).querySelectorAll(tabListTabClass);
tab.addEventListener("keydown", (e) => {
// Check if the key pressed is an arrow key or Home/End
if (["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft", "Home", "End"].includes(e.key)) {
e.preventDefault(); // Prevent default scrolling behavior when arrows keys clicked.
// Get the index of the currently focused link
let focusedIndex = Array.from(tabLinks).indexOf(document.activeElement);
// Calculate the new index based on the arrow key pressed
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
focusedIndex = (focusedIndex - 1 + tabLinks.length) % tabLinks.length; // Move left
} else if (["ArrowRight", "ArrowDown"].includes(e.key)) {
focusedIndex = (focusedIndex + 1) % tabLinks.length; // Move right
} else if (e.key === "Home") {
focusedIndex = 0; // Move to first tab
} else if (e.key === "End") {
focusedIndex = tabLinks.length - 1; // Move to last tab
}
// Focus and trigger event
tabLinks[focusedIndex].focus();
tabLinks[focusedIndex].click();
}
});
});
panels.forEach((panel) => {
const panelID = panel.getAttribute("id");
panel.setAttribute("aria-labelledby", `tab-${panelID}`);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("tabindex", "0");
panel.setAttribute("hidden", "");
if (panelID === URLFragment) {
panel.removeAttribute("hidden");
}
});
if (!URLFragment || !panelNames.includes(URLFragment)) {
tabs[0].setAttribute("aria-selected", "true");
tabs[0].removeAttribute("tabindex");
panels[0].removeAttribute("hidden");
panels[0].setAttribute("tabindex", "0");
}
});
// All TabList panels must have unique IDs. Check to see if duplicate ID's exist and alert developer.
// Note: This function may not be needed. Instead, simply having functionality so dependent on ID, may make this moot.
const allPanelID = [];
const tabPanels = document.querySelectorAll(tabListPanelClass);
tabPanels.forEach((panel) => {
const panelID = panel.id;
allPanelID.push(panelID);
});
allPanelID.forEach((element, index, array) => {
// Check if the current element is equal to any subsequent element
array.slice(index + 1).forEach((nextElement) => {
if (element === nextElement) {
console.log(`%c Warning: Duplicate Tablist ID found: #${element}. Tab panels must only contain unique ID values. `, "background: #ff0000; color: #fff");
}
});
});
}
initTablist();
})();Habitant Morbi
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Vestibulum Tortor Quam
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Malesuada Fames
Habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros porttitor, facilisis luctus, metus
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus
Reminder: Do not confuse a tabbed interface with a horizontal accordion—they are not the same. The key difference lies in their behavior. If you expect content sections to always close when a tab button is pressed, then please use the Accordion component. However, a tabbed UI should have only a few tabs. If there are too many, then you should consider a horizontal accordion instead.
Tab Placement: Respect the Natural Flow Anchor (Tab Placement: Respect the Natural Flow)
Placing tab controls below the content they manage creates a disconnect between the control and its content, resulting in a confusing and inaccessible experience across keyboard, mobile, and magnified views.
This pattern creates multiple usability and accessibility issues:
- Breaks expected keyboard flow: Users navigate left-to-right, top-to-bottom. When tabs are at the bottom, focus moves away from the active panel instead of into it, forcing users to tab backwards which is not predictable and violates WCAG 3.2.3 Consistent Navigation, WCAG 2.4.3 Focus Order
- Poor mobile usability: Content updates occur outside the user’s current view, making changes easy to miss.
- Screen magnification barriers: Magnified users are likely focused on the tabs; updated content appears above and may not be visible, and users may scroll the wrong direction.
- Focus and spatial disorientation: If panel height changes, activating a tab can shift the tabs’ position, causing users to lose their place.
- Violates established tab design conventions: Tabs are expected to precede and control the content that follows.
Example
<div class="tablist" data-fixed-height>
<ul class="tablist__list">
<li><a class="tablist__tab" href="#fixed-height-1">Habitant Morbi</a></li>
<li><a class="tablist__tab" href="#fixed-height-2">Vestibulum Tortor Quam</a></li>
<li><a class="tablist__tab" href="#fixed-height-3">Malesuada Fames</a></li>
</ul>
<div class="tablist__panel" id="fixed-height-1">
<div class="tablist__content">
<h2>Habitant Morbi</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
<p>Tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
<p>Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
<p>Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="fixed-height-2">
<div class="tablist__content">
<h2>Vestibulum Tortor Quam</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
<p>Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
<p>Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
<p>Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
<p>Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="fixed-height-3">
<div class="tablist__content">
<h2>Malesuada Fames</h2>
<p>Habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros porttitor, facilisis luctus, metus</p>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
<p>Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
<p>Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
<p>Malesuada fames ac turpis egestas. Vestibulum tortor quam. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
<p>Turpis egestas. Vestibulum tortor quam, titor, facilisis luctus, metus</p>
</div>
</div>
</div>/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
Dependencies: Sass
*/
.tablist {
// Component Variables
--tablist-color-active: #ffffff; // Active State
--tablist-color-default: #d3d3d3; // Default States
--tablist-color-interactive: #a9a9a9; // Interactive State
--tablist-color-selected: #6F00EF; // Selected States
// Breakpoints (CSS variables are not allowed in media queries - boo!)
$tablist-large: 55em; // Large Viewport
$tablist-medium: 48.625em; // Medium Viewport
color: var(--tablist-color-active);
position: relative;
&:has([aria-orientation]) {
@media(min-width: $tablist-large) {
display: grid;
grid-template-columns: calc(280rem /16) auto;
}
}
// Tablist Container (Optional)
&__container {
}
// Tablist Heading (Optional)
&__heading {
color: var(--tablist-color-selected);
font-size: 1.9rem;
text-align: center;
width: auto;
}
&__container, &__list {
align-self: center;
}
&__list {
display: flex;
flex-wrap: wrap;
gap: calc(4rem/16);
list-style: none;
margin: 0;
padding: 0;
&[aria-orientation] {
@media(min-width: $tablist-large) {
display: block;
}
}
[data-stack] & {
@media(max-width: $tablist-medium) {
display: block;
}
}
[data-scroll] & {
// This max-width can be changed depending on amount of list items you have.
@media(max-width: $tablist-medium) {
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: inline mandatory;
white-space: nowrap;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
&::-webkit-scrollbar {
height: calc(20rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(8rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
}
}
}
}
li {
flex-grow: 1;
scroll-snap-align: start;
@media(min-width: $tablist-medium) {
flex-grow: 0;
}
}
}
// Tab Button
&__tab {
background-color: var(--tablist-color-default);
color: #000;
display: block;
padding: 1em;
text-align: center;
text-decoration: none;
[data-animation] & {
@media (prefers-reduced-motion: no-preference) {
transition: 600ms;
}
}
&:hover {
background-color: var(--tablist-color-interactive);
}
&[aria-selected="true"] {
background-color: var(--tablist-color-selected);
color: var(--tablist-color-active);
}
}
// Tab Panel
&__panel {
background-color: var(--tablist-color-selected);
overflow: hidden;
padding: 1em;
position: relative;
[data-fixed-height] & {
max-height: calc(400em/16);
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
padding-inline-end: 0.5em;
&::-webkit-scrollbar {
width: calc(18rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
margin-block: 0.5em;
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(6rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
min-height: calc(100rem / 16);
}
}
}
}
// Tab Content
&__content {
position: relative;
// Your specific styles, if any.
[data-animation] & {
:not([hidden]) & {
inset-block-start: 0;
opacity: 1;
@starting-style {
inset-block-start: 3em;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transition: inset 600ms, opacity 300ms;
}
}
}
}
// Focus State
// Accessibility: Never remove focus outlines! Either redesign the outline or design an alternative focus state.
// However, a 3:1 ratio will be required. Learn: https://webaim.org/articles/contrast/
&__tab, &__panel {
&:focus {
outline-offset: -6px;
}
}
}/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
*/
(() => {
"use strict";
const initTablist = () => {
// Classes, data attributes, states, and strings.
const tabListClass = ".tablist";
const tabListListClass = ".tablist__list";
const tabListClassName = "tablist__tab";
const tabListTabClass = `.${tabListClassName}`;
const tabListDataDisableAnchor = "data-disable-anchor";
const tabListDataVertical = "data-vertical";
const tabListPanelClass = ".tablist__panel";
const tabLists = document.querySelectorAll(tabListClass);
const URLFragment = location.hash.slice(1);
// Display which version is in use via console:
console.log("%cTabList%cv2.0.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");
// Set up all Tablists on page
tabLists.forEach((list, e) => {
const index = e + 1;
list.setAttribute("id", `tablist-${index}`);
const listContainer = list.querySelector(tabListListClass);
listContainer.setAttribute("role", "tablist");
if (list.hasAttribute(tabListDataVertical)) {
listContainer.setAttribute("aria-orientation", "vertical");
}
// Get all tabs within tablist.
const tabs = list.querySelectorAll(tabListTabClass);
// Get all tabpanels within tablist.
const panels = list.querySelectorAll(tabListPanelClass);
// Array used to hold all panel IDs.
const panelNames = [];
// Prep each tab.
tabs.forEach((tab) => {
const tabID = tab.getAttribute("href").replace("#", "");
// Push all panel ID's to array.
panelNames.push(tabID);
// Setup each tab.
tab.parentElement.setAttribute("role", "presentation");
tab.setAttribute("aria-controls", tabID);
tab.setAttribute("id", `tab-${tabID}`);
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
// Highlight tab based on hash.
if (tabID === URLFragment) {
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
}
tab.addEventListener("click", (e) => {
const panelID = tab.getAttribute("href").replace("#", "");
tabs.forEach((tab) => {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide any open panels
const panels = tab.closest(tabListClass).querySelectorAll(tabListPanelClass);
const panelTarget = tab.closest(tabListClass).querySelector(`#${panelID}`);
panels.forEach((panel) => {
panel.setAttribute("hidden", "");
panel.removeAttribute("tabindex");
});
// Show selected panel
panelTarget.removeAttribute("hidden");
panelTarget.setAttribute("tabindex", "0");
// Highlight selected tab
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
if (!tab.closest(tabListClass).hasAttribute(tabListDataDisableAnchor)) {
history.pushState(null, null, `#${tabID}`);
}
e.preventDefault();
});
// Get tab links used in tablist.
const tabLinks = list.querySelector(tabListListClass).querySelectorAll(tabListTabClass);
tab.addEventListener("keydown", (e) => {
// Check if the key pressed is an arrow key or Home/End
if (["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft", "Home", "End"].includes(e.key)) {
e.preventDefault(); // Prevent default scrolling behavior when arrows keys clicked.
// Get the index of the currently focused link
let focusedIndex = Array.from(tabLinks).indexOf(document.activeElement);
// Calculate the new index based on the arrow key pressed
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
focusedIndex = (focusedIndex - 1 + tabLinks.length) % tabLinks.length; // Move left
} else if (["ArrowRight", "ArrowDown"].includes(e.key)) {
focusedIndex = (focusedIndex + 1) % tabLinks.length; // Move right
} else if (e.key === "Home") {
focusedIndex = 0; // Move to first tab
} else if (e.key === "End") {
focusedIndex = tabLinks.length - 1; // Move to last tab
}
// Focus and trigger event
tabLinks[focusedIndex].focus();
tabLinks[focusedIndex].click();
}
});
});
panels.forEach((panel) => {
const panelID = panel.getAttribute("id");
panel.setAttribute("aria-labelledby", `tab-${panelID}`);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("tabindex", "0");
panel.setAttribute("hidden", "");
if (panelID === URLFragment) {
panel.removeAttribute("hidden");
}
});
if (!URLFragment || !panelNames.includes(URLFragment)) {
tabs[0].setAttribute("aria-selected", "true");
tabs[0].removeAttribute("tabindex");
panels[0].removeAttribute("hidden");
panels[0].setAttribute("tabindex", "0");
}
});
// All TabList panels must have unique IDs. Check to see if duplicate ID's exist and alert developer.
// Note: This function may not be needed. Instead, simply having functionality so dependent on ID, may make this moot.
const allPanelID = [];
const tabPanels = document.querySelectorAll(tabListPanelClass);
tabPanels.forEach((panel) => {
const panelID = panel.id;
allPanelID.push(panelID);
});
allPanelID.forEach((element, index, array) => {
// Check if the current element is equal to any subsequent element
array.slice(index + 1).forEach((nextElement) => {
if (element === nextElement) {
console.log(`%c Warning: Duplicate Tablist ID found: #${element}. Tab panels must only contain unique ID values. `, "background: #ff0000; color: #fff");
}
});
});
}
initTablist();
})();Scrollbars & Accessibility Anchor (Scrollbars & Accessibility)
Scrollbars can look and work differently depending on the operating system and browser, which is normal. If you choose to style scrollbars, keep in mind that not all browsers support the same options, so the look and feel may change. For example, Safari may hide scrollbars completely, which is expected. For accessibility, ensure the following:
- The scrubber, track, and background must each have a 3:1 contrast ratio with one another. For example, the scrubber must have a 3:1 contrast with the track, and the track must have a 3:1 contrast with the background beneath it. In some cases, it may be advisable to have the scrubber meet a 4.5:1 ratio with the track, for more solid usability.
- The hover state of the scrubber must meet a 3:1 contrast ratio with the track.
- The scrollbar must always include a 24px by 24px target area size. This isn't an issue for horizontal spacing on horizontal scrollbars, but be mindful of vertical spacing—if the scrollbar is too close to other interactive elements above or below it, it may be harder to use and will fail Target Size (Minimum) (Level AA). The opposite would be true on vertial scrollbars.
- There are no recommended dimensions for scrollbar scrubber height (horizontal) or width (vertical), but larger sizes would obviously be more noticeable and easier to use.
- While customizing scrollbars is permitted it is not generally recommended. Using native scrollbars is usually the most usable and accessible option. See Don't use custom CSS scrollbars for more details on why.
Tristique Senectus
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Vestibulum Tortor Quam
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Turpis Egestas
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Quisque Sit Amet
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Note: Tabs can be designed to scroll on mobile and display inline on desktop. To test the scrollable tabs, resize the screen to a smaller width. When designing, ensure a partial tab is visible at the edge to indicate scrollability and enhance usability.
Reminder: Do not confuse a tabbed interface with a horizontal accordion—they are not the same. The key difference lies in their behavior. If you expect content sections to always close when a tab button is pressed, then please use the Accordion component. However, a tabbed UI should have only a few tabs. If there are too many, then you should consider a horizontal accordion instead.
Tab Placement: Respect the Natural Flow Anchor (Tab Placement: Respect the Natural Flow)
Placing tab controls below the content they manage creates a disconnect between the control and its content, resulting in a confusing and inaccessible experience across keyboard, mobile, and magnified views.
This pattern creates multiple usability and accessibility issues:
- Breaks expected keyboard flow: Users navigate left-to-right, top-to-bottom. When tabs are at the bottom, focus moves away from the active panel instead of into it, forcing users to tab backwards which is not predictable and violates WCAG 3.2.3 Consistent Navigation, WCAG 2.4.3 Focus Order
- Poor mobile usability: Content updates occur outside the user’s current view, making changes easy to miss.
- Screen magnification barriers: Magnified users are likely focused on the tabs; updated content appears above and may not be visible, and users may scroll the wrong direction.
- Focus and spatial disorientation: If panel height changes, activating a tab can shift the tabs’ position, causing users to lose their place.
- Violates established tab design conventions: Tabs are expected to precede and control the content that follows.
Example
<div class="tablist" data-scroll>
<ul class="tablist__list">
<li><a class="tablist__tab" href="#scroll-list-1">Tristique Senectus</a></li>
<li><a class="tablist__tab" href="#scroll-list-2">Vestibulum Tortor Quam</a></li>
<li><a class="tablist__tab" href="#scroll-list-3">Turpis Egestas</a></li>
<li><a class="tablist__tab" href="#scroll-list-4">Quisque Sit Amet</a></li>
</ul>
<div class="tablist__panel" id="scroll-list-1">
<div class="tablist__content">
<h2>Tristique Senectus</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="scroll-list-2">
<div class="tablist__content">
<h2>Vestibulum Tortor Quam</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="scroll-list-3">
<div class="tablist__content">
<h2>Turpis Egestas</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="scroll-list-4">
<div class="tablist__content">
<h2>Quisque Sit Amet</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
</div>/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
Dependencies: Sass
*/
.tablist {
// Component Variables
--tablist-color-active: #ffffff; // Active State
--tablist-color-default: #d3d3d3; // Default States
--tablist-color-interactive: #a9a9a9; // Interactive State
--tablist-color-selected: #6F00EF; // Selected States
// Breakpoints (CSS variables are not allowed in media queries - boo!)
$tablist-large: 55em; // Large Viewport
$tablist-medium: 48.625em; // Medium Viewport
color: var(--tablist-color-active);
position: relative;
&:has([aria-orientation]) {
@media(min-width: $tablist-large) {
display: grid;
grid-template-columns: calc(280rem /16) auto;
}
}
// Tablist Container (Optional)
&__container {
}
// Tablist Heading (Optional)
&__heading {
color: var(--tablist-color-selected);
font-size: 1.9rem;
text-align: center;
width: auto;
}
&__container, &__list {
align-self: center;
}
&__list {
display: flex;
flex-wrap: wrap;
gap: calc(4rem/16);
list-style: none;
margin: 0;
padding: 0;
&[aria-orientation] {
@media(min-width: $tablist-large) {
display: block;
}
}
[data-stack] & {
@media(max-width: $tablist-medium) {
display: block;
}
}
[data-scroll] & {
// This max-width can be changed depending on amount of list items you have.
@media(max-width: $tablist-medium) {
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: inline mandatory;
white-space: nowrap;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
&::-webkit-scrollbar {
height: calc(20rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(8rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
}
}
}
}
li {
flex-grow: 1;
scroll-snap-align: start;
@media(min-width: $tablist-medium) {
flex-grow: 0;
}
}
}
// Tab Button
&__tab {
background-color: var(--tablist-color-default);
color: #000;
display: block;
padding: 1em;
text-align: center;
text-decoration: none;
[data-animation] & {
@media (prefers-reduced-motion: no-preference) {
transition: 600ms;
}
}
&:hover {
background-color: var(--tablist-color-interactive);
}
&[aria-selected="true"] {
background-color: var(--tablist-color-selected);
color: var(--tablist-color-active);
}
}
// Tab Panel
&__panel {
background-color: var(--tablist-color-selected);
overflow: hidden;
padding: 1em;
position: relative;
[data-fixed-height] & {
max-height: calc(400em/16);
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
padding-inline-end: 0.5em;
&::-webkit-scrollbar {
width: calc(18rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
margin-block: 0.5em;
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(6rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
min-height: calc(100rem / 16);
}
}
}
}
// Tab Content
&__content {
position: relative;
// Your specific styles, if any.
[data-animation] & {
:not([hidden]) & {
inset-block-start: 0;
opacity: 1;
@starting-style {
inset-block-start: 3em;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transition: inset 600ms, opacity 300ms;
}
}
}
}
// Focus State
// Accessibility: Never remove focus outlines! Either redesign the outline or design an alternative focus state.
// However, a 3:1 ratio will be required. Learn: https://webaim.org/articles/contrast/
&__tab, &__panel {
&:focus {
outline-offset: -6px;
}
}
}/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
*/
(() => {
"use strict";
const initTablist = () => {
// Classes, data attributes, states, and strings.
const tabListClass = ".tablist";
const tabListListClass = ".tablist__list";
const tabListClassName = "tablist__tab";
const tabListTabClass = `.${tabListClassName}`;
const tabListDataDisableAnchor = "data-disable-anchor";
const tabListDataVertical = "data-vertical";
const tabListPanelClass = ".tablist__panel";
const tabLists = document.querySelectorAll(tabListClass);
const URLFragment = location.hash.slice(1);
// Display which version is in use via console:
console.log("%cTabList%cv2.0.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");
// Set up all Tablists on page
tabLists.forEach((list, e) => {
const index = e + 1;
list.setAttribute("id", `tablist-${index}`);
const listContainer = list.querySelector(tabListListClass);
listContainer.setAttribute("role", "tablist");
if (list.hasAttribute(tabListDataVertical)) {
listContainer.setAttribute("aria-orientation", "vertical");
}
// Get all tabs within tablist.
const tabs = list.querySelectorAll(tabListTabClass);
// Get all tabpanels within tablist.
const panels = list.querySelectorAll(tabListPanelClass);
// Array used to hold all panel IDs.
const panelNames = [];
// Prep each tab.
tabs.forEach((tab) => {
const tabID = tab.getAttribute("href").replace("#", "");
// Push all panel ID's to array.
panelNames.push(tabID);
// Setup each tab.
tab.parentElement.setAttribute("role", "presentation");
tab.setAttribute("aria-controls", tabID);
tab.setAttribute("id", `tab-${tabID}`);
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
// Highlight tab based on hash.
if (tabID === URLFragment) {
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
}
tab.addEventListener("click", (e) => {
const panelID = tab.getAttribute("href").replace("#", "");
tabs.forEach((tab) => {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide any open panels
const panels = tab.closest(tabListClass).querySelectorAll(tabListPanelClass);
const panelTarget = tab.closest(tabListClass).querySelector(`#${panelID}`);
panels.forEach((panel) => {
panel.setAttribute("hidden", "");
panel.removeAttribute("tabindex");
});
// Show selected panel
panelTarget.removeAttribute("hidden");
panelTarget.setAttribute("tabindex", "0");
// Highlight selected tab
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
if (!tab.closest(tabListClass).hasAttribute(tabListDataDisableAnchor)) {
history.pushState(null, null, `#${tabID}`);
}
e.preventDefault();
});
// Get tab links used in tablist.
const tabLinks = list.querySelector(tabListListClass).querySelectorAll(tabListTabClass);
tab.addEventListener("keydown", (e) => {
// Check if the key pressed is an arrow key or Home/End
if (["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft", "Home", "End"].includes(e.key)) {
e.preventDefault(); // Prevent default scrolling behavior when arrows keys clicked.
// Get the index of the currently focused link
let focusedIndex = Array.from(tabLinks).indexOf(document.activeElement);
// Calculate the new index based on the arrow key pressed
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
focusedIndex = (focusedIndex - 1 + tabLinks.length) % tabLinks.length; // Move left
} else if (["ArrowRight", "ArrowDown"].includes(e.key)) {
focusedIndex = (focusedIndex + 1) % tabLinks.length; // Move right
} else if (e.key === "Home") {
focusedIndex = 0; // Move to first tab
} else if (e.key === "End") {
focusedIndex = tabLinks.length - 1; // Move to last tab
}
// Focus and trigger event
tabLinks[focusedIndex].focus();
tabLinks[focusedIndex].click();
}
});
});
panels.forEach((panel) => {
const panelID = panel.getAttribute("id");
panel.setAttribute("aria-labelledby", `tab-${panelID}`);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("tabindex", "0");
panel.setAttribute("hidden", "");
if (panelID === URLFragment) {
panel.removeAttribute("hidden");
}
});
if (!URLFragment || !panelNames.includes(URLFragment)) {
tabs[0].setAttribute("aria-selected", "true");
tabs[0].removeAttribute("tabindex");
panels[0].removeAttribute("hidden");
panels[0].setAttribute("tabindex", "0");
}
});
// All TabList panels must have unique IDs. Check to see if duplicate ID's exist and alert developer.
// Note: This function may not be needed. Instead, simply having functionality so dependent on ID, may make this moot.
const allPanelID = [];
const tabPanels = document.querySelectorAll(tabListPanelClass);
tabPanels.forEach((panel) => {
const panelID = panel.id;
allPanelID.push(panelID);
});
allPanelID.forEach((element, index, array) => {
// Check if the current element is equal to any subsequent element
array.slice(index + 1).forEach((nextElement) => {
if (element === nextElement) {
console.log(`%c Warning: Duplicate Tablist ID found: #${element}. Tab panels must only contain unique ID values. `, "background: #ff0000; color: #fff");
}
});
});
}
initTablist();
})();Scrollbars & Accessibility Anchor (Scrollbars & Accessibility)
Scrollbars can look and work differently depending on the operating system and browser, which is normal. If you choose to style scrollbars, keep in mind that not all browsers support the same options, so the look and feel may change. For example, Safari may hide scrollbars completely, which is expected. For accessibility, ensure the following:
- The scrubber, track, and background must each have a 3:1 contrast ratio with one another. For example, the scrubber must have a 3:1 contrast with the track, and the track must have a 3:1 contrast with the background beneath it. In some cases, it may be advisable to have the scrubber meet a 4.5:1 ratio with the track, for more solid usability.
- The hover state of the scrubber must meet a 3:1 contrast ratio with the track.
- The scrollbar must always include a 24px by 24px target area size. This isn't an issue for horizontal spacing on horizontal scrollbars, but be mindful of vertical spacing—if the scrollbar is too close to other interactive elements above or below it, it may be harder to use and will fail Target Size (Minimum) (Level AA). The opposite would be true on vertial scrollbars.
- There are no recommended dimensions for scrollbar scrubber height (horizontal) or width (vertical), but larger sizes would obviously be more noticeable and easier to use.
- While customizing scrollbars is permitted it is not generally recommended. Using native scrollbars is usually the most usable and accessible option. See Don't use custom CSS scrollbars for more details on why.
Habitant morbi
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Vestibulum Tortor Quam
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Malesuada Fames
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Note: Tabs can stack on mobile and display inline on desktop. To see the stacked tabs, resize the screen to a smaller width.
Reminder: Do not confuse a tabbed interface with a horizontal accordion—they are not the same. The key difference lies in their behavior. If you expect content sections to always close when a tab button is pressed, then please use the Accordion component. However, a tabbed UI should have only a few tabs. If there are too many, then you should consider a horizontal accordion instead.
Tab Placement: Respect the Natural Flow Anchor (Tab Placement: Respect the Natural Flow)
Placing tab controls below the content they manage creates a disconnect between the control and its content, resulting in a confusing and inaccessible experience across keyboard, mobile, and magnified views.
This pattern creates multiple usability and accessibility issues:
- Breaks expected keyboard flow: Users navigate left-to-right, top-to-bottom. When tabs are at the bottom, focus moves away from the active panel instead of into it, forcing users to tab backwards which is not predictable and violates WCAG 3.2.3 Consistent Navigation, WCAG 2.4.3 Focus Order
- Poor mobile usability: Content updates occur outside the user’s current view, making changes easy to miss.
- Screen magnification barriers: Magnified users are likely focused on the tabs; updated content appears above and may not be visible, and users may scroll the wrong direction.
- Focus and spatial disorientation: If panel height changes, activating a tab can shift the tabs’ position, causing users to lose their place.
- Violates established tab design conventions: Tabs are expected to precede and control the content that follows.
Example
<div class="tablist" data-stack>
<ul class="tablist__list">
<li><a class="tablist__tab" href="#mobile-stack-1">Habitant Morbi</a></li>
<li><a class="tablist__tab" href="#mobile-stack-2">Vestibulum Tortor Quam</a></li>
<li><a class="tablist__tab" href="#mobile-stack-3">Malesuada Fames</a></li>
</ul>
<div class="tablist__panel" id="mobile-stack-1">
<div class="tablist__content">
<h2>Habitant morbi</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="mobile-stack-2">
<div class="tablist__content">
<h2>Vestibulum Tortor Quam</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="mobile-stack-3">
<div class="tablist__content">
<h2>Malesuada Fames</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
</div>/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
Dependencies: Sass
*/
.tablist {
// Component Variables
--tablist-color-active: #ffffff; // Active State
--tablist-color-default: #d3d3d3; // Default States
--tablist-color-interactive: #a9a9a9; // Interactive State
--tablist-color-selected: #6F00EF; // Selected States
// Breakpoints (CSS variables are not allowed in media queries - boo!)
$tablist-large: 55em; // Large Viewport
$tablist-medium: 48.625em; // Medium Viewport
color: var(--tablist-color-active);
position: relative;
&:has([aria-orientation]) {
@media(min-width: $tablist-large) {
display: grid;
grid-template-columns: calc(280rem /16) auto;
}
}
// Tablist Container (Optional)
&__container {
}
// Tablist Heading (Optional)
&__heading {
color: var(--tablist-color-selected);
font-size: 1.9rem;
text-align: center;
width: auto;
}
&__container, &__list {
align-self: center;
}
&__list {
display: flex;
flex-wrap: wrap;
gap: calc(4rem/16);
list-style: none;
margin: 0;
padding: 0;
&[aria-orientation] {
@media(min-width: $tablist-large) {
display: block;
}
}
[data-stack] & {
@media(max-width: $tablist-medium) {
display: block;
}
}
[data-scroll] & {
// This max-width can be changed depending on amount of list items you have.
@media(max-width: $tablist-medium) {
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: inline mandatory;
white-space: nowrap;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
&::-webkit-scrollbar {
height: calc(20rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(8rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
}
}
}
}
li {
flex-grow: 1;
scroll-snap-align: start;
@media(min-width: $tablist-medium) {
flex-grow: 0;
}
}
}
// Tab Button
&__tab {
background-color: var(--tablist-color-default);
color: #000;
display: block;
padding: 1em;
text-align: center;
text-decoration: none;
[data-animation] & {
@media (prefers-reduced-motion: no-preference) {
transition: 600ms;
}
}
&:hover {
background-color: var(--tablist-color-interactive);
}
&[aria-selected="true"] {
background-color: var(--tablist-color-selected);
color: var(--tablist-color-active);
}
}
// Tab Panel
&__panel {
background-color: var(--tablist-color-selected);
overflow: hidden;
padding: 1em;
position: relative;
[data-fixed-height] & {
max-height: calc(400em/16);
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
padding-inline-end: 0.5em;
&::-webkit-scrollbar {
width: calc(18rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
margin-block: 0.5em;
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(6rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
min-height: calc(100rem / 16);
}
}
}
}
// Tab Content
&__content {
position: relative;
// Your specific styles, if any.
[data-animation] & {
:not([hidden]) & {
inset-block-start: 0;
opacity: 1;
@starting-style {
inset-block-start: 3em;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transition: inset 600ms, opacity 300ms;
}
}
}
}
// Focus State
// Accessibility: Never remove focus outlines! Either redesign the outline or design an alternative focus state.
// However, a 3:1 ratio will be required. Learn: https://webaim.org/articles/contrast/
&__tab, &__panel {
&:focus {
outline-offset: -6px;
}
}
}/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
*/
(() => {
"use strict";
const initTablist = () => {
// Classes, data attributes, states, and strings.
const tabListClass = ".tablist";
const tabListListClass = ".tablist__list";
const tabListClassName = "tablist__tab";
const tabListTabClass = `.${tabListClassName}`;
const tabListDataDisableAnchor = "data-disable-anchor";
const tabListDataVertical = "data-vertical";
const tabListPanelClass = ".tablist__panel";
const tabLists = document.querySelectorAll(tabListClass);
const URLFragment = location.hash.slice(1);
// Display which version is in use via console:
console.log("%cTabList%cv2.0.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");
// Set up all Tablists on page
tabLists.forEach((list, e) => {
const index = e + 1;
list.setAttribute("id", `tablist-${index}`);
const listContainer = list.querySelector(tabListListClass);
listContainer.setAttribute("role", "tablist");
if (list.hasAttribute(tabListDataVertical)) {
listContainer.setAttribute("aria-orientation", "vertical");
}
// Get all tabs within tablist.
const tabs = list.querySelectorAll(tabListTabClass);
// Get all tabpanels within tablist.
const panels = list.querySelectorAll(tabListPanelClass);
// Array used to hold all panel IDs.
const panelNames = [];
// Prep each tab.
tabs.forEach((tab) => {
const tabID = tab.getAttribute("href").replace("#", "");
// Push all panel ID's to array.
panelNames.push(tabID);
// Setup each tab.
tab.parentElement.setAttribute("role", "presentation");
tab.setAttribute("aria-controls", tabID);
tab.setAttribute("id", `tab-${tabID}`);
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
// Highlight tab based on hash.
if (tabID === URLFragment) {
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
}
tab.addEventListener("click", (e) => {
const panelID = tab.getAttribute("href").replace("#", "");
tabs.forEach((tab) => {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide any open panels
const panels = tab.closest(tabListClass).querySelectorAll(tabListPanelClass);
const panelTarget = tab.closest(tabListClass).querySelector(`#${panelID}`);
panels.forEach((panel) => {
panel.setAttribute("hidden", "");
panel.removeAttribute("tabindex");
});
// Show selected panel
panelTarget.removeAttribute("hidden");
panelTarget.setAttribute("tabindex", "0");
// Highlight selected tab
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
if (!tab.closest(tabListClass).hasAttribute(tabListDataDisableAnchor)) {
history.pushState(null, null, `#${tabID}`);
}
e.preventDefault();
});
// Get tab links used in tablist.
const tabLinks = list.querySelector(tabListListClass).querySelectorAll(tabListTabClass);
tab.addEventListener("keydown", (e) => {
// Check if the key pressed is an arrow key or Home/End
if (["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft", "Home", "End"].includes(e.key)) {
e.preventDefault(); // Prevent default scrolling behavior when arrows keys clicked.
// Get the index of the currently focused link
let focusedIndex = Array.from(tabLinks).indexOf(document.activeElement);
// Calculate the new index based on the arrow key pressed
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
focusedIndex = (focusedIndex - 1 + tabLinks.length) % tabLinks.length; // Move left
} else if (["ArrowRight", "ArrowDown"].includes(e.key)) {
focusedIndex = (focusedIndex + 1) % tabLinks.length; // Move right
} else if (e.key === "Home") {
focusedIndex = 0; // Move to first tab
} else if (e.key === "End") {
focusedIndex = tabLinks.length - 1; // Move to last tab
}
// Focus and trigger event
tabLinks[focusedIndex].focus();
tabLinks[focusedIndex].click();
}
});
});
panels.forEach((panel) => {
const panelID = panel.getAttribute("id");
panel.setAttribute("aria-labelledby", `tab-${panelID}`);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("tabindex", "0");
panel.setAttribute("hidden", "");
if (panelID === URLFragment) {
panel.removeAttribute("hidden");
}
});
if (!URLFragment || !panelNames.includes(URLFragment)) {
tabs[0].setAttribute("aria-selected", "true");
tabs[0].removeAttribute("tabindex");
panels[0].removeAttribute("hidden");
panels[0].setAttribute("tabindex", "0");
}
});
// All TabList panels must have unique IDs. Check to see if duplicate ID's exist and alert developer.
// Note: This function may not be needed. Instead, simply having functionality so dependent on ID, may make this moot.
const allPanelID = [];
const tabPanels = document.querySelectorAll(tabListPanelClass);
tabPanels.forEach((panel) => {
const panelID = panel.id;
allPanelID.push(panelID);
});
allPanelID.forEach((element, index, array) => {
// Check if the current element is equal to any subsequent element
array.slice(index + 1).forEach((nextElement) => {
if (element === nextElement) {
console.log(`%c Warning: Duplicate Tablist ID found: #${element}. Tab panels must only contain unique ID values. `, "background: #ff0000; color: #fff");
}
});
});
}
initTablist();
})();Tristique Senectus
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Vestibulum Tortor Quam
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Turpis Egestas
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Quisque Sit Amet
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Volutpat
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Note: In this example, tabs will be placed vertically on desktop and display inline on mobile. To test this, resize the screen to a smaller width.
Reminder: Do not confuse a tabbed interface with a horizontal accordion—they are not the same. The key difference lies in their behavior. If you expect content sections to always close when a tab button is pressed, then please use the Accordion component. However, a tabbed UI should have only a few tabs. If there are too many, then you should consider a horizontal accordion instead.
Tab Placement: Respect the Natural Flow Anchor (Tab Placement: Respect the Natural Flow)
Placing tab controls below the content they manage creates a disconnect between the control and its content, resulting in a confusing and inaccessible experience across keyboard, mobile, and magnified views.
This pattern creates multiple usability and accessibility issues:
- Breaks expected keyboard flow: Users navigate left-to-right, top-to-bottom. When tabs are at the bottom, focus moves away from the active panel instead of into it, forcing users to tab backwards which is not predictable and violates WCAG 3.2.3 Consistent Navigation, WCAG 2.4.3 Focus Order
- Poor mobile usability: Content updates occur outside the user’s current view, making changes easy to miss.
- Screen magnification barriers: Magnified users are likely focused on the tabs; updated content appears above and may not be visible, and users may scroll the wrong direction.
- Focus and spatial disorientation: If panel height changes, activating a tab can shift the tabs’ position, causing users to lose their place.
- Violates established tab design conventions: Tabs are expected to precede and control the content that follows.
Example
<div class="tablist" data-vertical>
<ul class="tablist__list">
<li><a class="tablist__tab" href="#vertical-tab-1">Tristique Senectus</a></li>
<li><a class="tablist__tab" href="#vertical-tab-2">Vestibulum Tortor Quam</a></li>
<li><a class="tablist__tab" href="#vertical-tab-3">Turpis Egestas</a></li>
<li><a class="tablist__tab" href="#vertical-tab-4">Quisque Sit Amet</a></li>
<li><a class="tablist__tab" href="#vertical-tab-5">Volutpat</a></li>
</ul>
<div class="tablist__panel" id="vertical-tab-1">
<div class="tablist__content">
<h2>Tristique Senectus</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="vertical-tab-2">
<div class="tablist__content">
<h2>Vestibulum Tortor Quam</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="vertical-tab-3">
<div class="tablist__content">
<h2>Turpis Egestas</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="vertical-tab-4">
<div class="tablist__content">
<h2>Quisque Sit Amet</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="vertical-tab-5">
<div class="tablist__content">
<h2>Volutpat</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
</div>/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
Dependencies: Sass
*/
.tablist {
// Component Variables
--tablist-color-active: #ffffff; // Active State
--tablist-color-default: #d3d3d3; // Default States
--tablist-color-interactive: #a9a9a9; // Interactive State
--tablist-color-selected: #6F00EF; // Selected States
// Breakpoints (CSS variables are not allowed in media queries - boo!)
$tablist-large: 55em; // Large Viewport
$tablist-medium: 48.625em; // Medium Viewport
color: var(--tablist-color-active);
position: relative;
&:has([aria-orientation]) {
@media(min-width: $tablist-large) {
display: grid;
grid-template-columns: calc(280rem /16) auto;
}
}
// Tablist Container (Optional)
&__container {
}
// Tablist Heading (Optional)
&__heading {
color: var(--tablist-color-selected);
font-size: 1.9rem;
text-align: center;
width: auto;
}
&__container, &__list {
align-self: center;
}
&__list {
display: flex;
flex-wrap: wrap;
gap: calc(4rem/16);
list-style: none;
margin: 0;
padding: 0;
&[aria-orientation] {
@media(min-width: $tablist-large) {
display: block;
}
}
[data-stack] & {
@media(max-width: $tablist-medium) {
display: block;
}
}
[data-scroll] & {
// This max-width can be changed depending on amount of list items you have.
@media(max-width: $tablist-medium) {
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: inline mandatory;
white-space: nowrap;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
&::-webkit-scrollbar {
height: calc(20rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(8rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
}
}
}
}
li {
flex-grow: 1;
scroll-snap-align: start;
@media(min-width: $tablist-medium) {
flex-grow: 0;
}
}
}
// Tab Button
&__tab {
background-color: var(--tablist-color-default);
color: #000;
display: block;
padding: 1em;
text-align: center;
text-decoration: none;
[data-animation] & {
@media (prefers-reduced-motion: no-preference) {
transition: 600ms;
}
}
&:hover {
background-color: var(--tablist-color-interactive);
}
&[aria-selected="true"] {
background-color: var(--tablist-color-selected);
color: var(--tablist-color-active);
}
}
// Tab Panel
&__panel {
background-color: var(--tablist-color-selected);
overflow: hidden;
padding: 1em;
position: relative;
[data-fixed-height] & {
max-height: calc(400em/16);
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
padding-inline-end: 0.5em;
&::-webkit-scrollbar {
width: calc(18rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
margin-block: 0.5em;
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(6rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
min-height: calc(100rem / 16);
}
}
}
}
// Tab Content
&__content {
position: relative;
// Your specific styles, if any.
[data-animation] & {
:not([hidden]) & {
inset-block-start: 0;
opacity: 1;
@starting-style {
inset-block-start: 3em;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transition: inset 600ms, opacity 300ms;
}
}
}
}
// Focus State
// Accessibility: Never remove focus outlines! Either redesign the outline or design an alternative focus state.
// However, a 3:1 ratio will be required. Learn: https://webaim.org/articles/contrast/
&__tab, &__panel {
&:focus {
outline-offset: -6px;
}
}
}/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
*/
(() => {
"use strict";
const initTablist = () => {
// Classes, data attributes, states, and strings.
const tabListClass = ".tablist";
const tabListListClass = ".tablist__list";
const tabListClassName = "tablist__tab";
const tabListTabClass = `.${tabListClassName}`;
const tabListDataDisableAnchor = "data-disable-anchor";
const tabListDataVertical = "data-vertical";
const tabListPanelClass = ".tablist__panel";
const tabLists = document.querySelectorAll(tabListClass);
const URLFragment = location.hash.slice(1);
// Display which version is in use via console:
console.log("%cTabList%cv2.0.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");
// Set up all Tablists on page
tabLists.forEach((list, e) => {
const index = e + 1;
list.setAttribute("id", `tablist-${index}`);
const listContainer = list.querySelector(tabListListClass);
listContainer.setAttribute("role", "tablist");
if (list.hasAttribute(tabListDataVertical)) {
listContainer.setAttribute("aria-orientation", "vertical");
}
// Get all tabs within tablist.
const tabs = list.querySelectorAll(tabListTabClass);
// Get all tabpanels within tablist.
const panels = list.querySelectorAll(tabListPanelClass);
// Array used to hold all panel IDs.
const panelNames = [];
// Prep each tab.
tabs.forEach((tab) => {
const tabID = tab.getAttribute("href").replace("#", "");
// Push all panel ID's to array.
panelNames.push(tabID);
// Setup each tab.
tab.parentElement.setAttribute("role", "presentation");
tab.setAttribute("aria-controls", tabID);
tab.setAttribute("id", `tab-${tabID}`);
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
// Highlight tab based on hash.
if (tabID === URLFragment) {
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
}
tab.addEventListener("click", (e) => {
const panelID = tab.getAttribute("href").replace("#", "");
tabs.forEach((tab) => {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide any open panels
const panels = tab.closest(tabListClass).querySelectorAll(tabListPanelClass);
const panelTarget = tab.closest(tabListClass).querySelector(`#${panelID}`);
panels.forEach((panel) => {
panel.setAttribute("hidden", "");
panel.removeAttribute("tabindex");
});
// Show selected panel
panelTarget.removeAttribute("hidden");
panelTarget.setAttribute("tabindex", "0");
// Highlight selected tab
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
if (!tab.closest(tabListClass).hasAttribute(tabListDataDisableAnchor)) {
history.pushState(null, null, `#${tabID}`);
}
e.preventDefault();
});
// Get tab links used in tablist.
const tabLinks = list.querySelector(tabListListClass).querySelectorAll(tabListTabClass);
tab.addEventListener("keydown", (e) => {
// Check if the key pressed is an arrow key or Home/End
if (["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft", "Home", "End"].includes(e.key)) {
e.preventDefault(); // Prevent default scrolling behavior when arrows keys clicked.
// Get the index of the currently focused link
let focusedIndex = Array.from(tabLinks).indexOf(document.activeElement);
// Calculate the new index based on the arrow key pressed
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
focusedIndex = (focusedIndex - 1 + tabLinks.length) % tabLinks.length; // Move left
} else if (["ArrowRight", "ArrowDown"].includes(e.key)) {
focusedIndex = (focusedIndex + 1) % tabLinks.length; // Move right
} else if (e.key === "Home") {
focusedIndex = 0; // Move to first tab
} else if (e.key === "End") {
focusedIndex = tabLinks.length - 1; // Move to last tab
}
// Focus and trigger event
tabLinks[focusedIndex].focus();
tabLinks[focusedIndex].click();
}
});
});
panels.forEach((panel) => {
const panelID = panel.getAttribute("id");
panel.setAttribute("aria-labelledby", `tab-${panelID}`);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("tabindex", "0");
panel.setAttribute("hidden", "");
if (panelID === URLFragment) {
panel.removeAttribute("hidden");
}
});
if (!URLFragment || !panelNames.includes(URLFragment)) {
tabs[0].setAttribute("aria-selected", "true");
tabs[0].removeAttribute("tabindex");
panels[0].removeAttribute("hidden");
panels[0].setAttribute("tabindex", "0");
}
});
// All TabList panels must have unique IDs. Check to see if duplicate ID's exist and alert developer.
// Note: This function may not be needed. Instead, simply having functionality so dependent on ID, may make this moot.
const allPanelID = [];
const tabPanels = document.querySelectorAll(tabListPanelClass);
tabPanels.forEach((panel) => {
const panelID = panel.id;
allPanelID.push(panelID);
});
allPanelID.forEach((element, index, array) => {
// Check if the current element is equal to any subsequent element
array.slice(index + 1).forEach((nextElement) => {
if (element === nextElement) {
console.log(`%c Warning: Duplicate Tablist ID found: #${element}. Tab panels must only contain unique ID values. `, "background: #ff0000; color: #fff");
}
});
});
}
initTablist();
})();Ipsums for You
Tristique Senectus
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Vestibulum Tortor Quam
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Turpis Egestas
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus
Reminder: Do not confuse a tabbed interface with a horizontal accordion—they are not the same. The key difference lies in their behavior. If you expect content sections to always close when a tab button is pressed, then please use the Accordion component. However, a tabbed UI should have only a few tabs. If there are too many, then you should consider a horizontal accordion instead.
Tab Placement: Respect the Natural Flow Anchor (Tab Placement: Respect the Natural Flow)
Placing tab controls below the content they manage creates a disconnect between the control and its content, resulting in a confusing and inaccessible experience across keyboard, mobile, and magnified views.
This pattern creates multiple usability and accessibility issues:
- Breaks expected keyboard flow: Users navigate left-to-right, top-to-bottom. When tabs are at the bottom, focus moves away from the active panel instead of into it, forcing users to tab backwards which is not predictable and violates WCAG 3.2.3 Consistent Navigation, WCAG 2.4.3 Focus Order
- Poor mobile usability: Content updates occur outside the user’s current view, making changes easy to miss.
- Screen magnification barriers: Magnified users are likely focused on the tabs; updated content appears above and may not be visible, and users may scroll the wrong direction.
- Focus and spatial disorientation: If panel height changes, activating a tab can shift the tabs’ position, causing users to lose their place.
- Violates established tab design conventions: Tabs are expected to precede and control the content that follows.
Example
<div class="tablist" data-vertical>
<div class="tablist__container">
<h2 class="tablist__heading">Ipsums for You</h2>
<ul class="tablist__list">
<li><a class="tablist__tab" href="#vertical-heading-1">Tristique Senectus</a></li>
<li><a class="tablist__tab" href="#vertical-heading-2">Vestibulum Tortor Quam</a></li>
<li><a class="tablist__tab" href="#vertical-heading-3">Turpis Egestas</a></li>
</ul>
</div>
<div class="tablist__panel" id="vertical-heading-1">
<div class="tablist__content">
<h2>Tristique Senectus</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="vertical-heading-2">
<div class="tablist__content">
<h2>Vestibulum Tortor Quam</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
<div class="tablist__panel" id="vertical-heading-3">
<div class="tablist__content">
<h2>Turpis Egestas</h2>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis. Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus</p>
</div>
</div>
</div>/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
Dependencies: Sass
*/
.tablist {
// Component Variables
--tablist-color-active: #ffffff; // Active State
--tablist-color-default: #d3d3d3; // Default States
--tablist-color-interactive: #a9a9a9; // Interactive State
--tablist-color-selected: #6F00EF; // Selected States
// Breakpoints (CSS variables are not allowed in media queries - boo!)
$tablist-large: 55em; // Large Viewport
$tablist-medium: 48.625em; // Medium Viewport
color: var(--tablist-color-active);
position: relative;
&:has([aria-orientation]) {
@media(min-width: $tablist-large) {
display: grid;
grid-template-columns: calc(280rem /16) auto;
}
}
// Tablist Container (Optional)
&__container {
}
// Tablist Heading (Optional)
&__heading {
color: var(--tablist-color-selected);
font-size: 1.9rem;
text-align: center;
width: auto;
}
&__container, &__list {
align-self: center;
}
&__list {
display: flex;
flex-wrap: wrap;
gap: calc(4rem/16);
list-style: none;
margin: 0;
padding: 0;
&[aria-orientation] {
@media(min-width: $tablist-large) {
display: block;
}
}
[data-stack] & {
@media(max-width: $tablist-medium) {
display: block;
}
}
[data-scroll] & {
// This max-width can be changed depending on amount of list items you have.
@media(max-width: $tablist-medium) {
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: inline mandatory;
white-space: nowrap;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
&::-webkit-scrollbar {
height: calc(20rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(8rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
}
}
}
}
li {
flex-grow: 1;
scroll-snap-align: start;
@media(min-width: $tablist-medium) {
flex-grow: 0;
}
}
}
// Tab Button
&__tab {
background-color: var(--tablist-color-default);
color: #000;
display: block;
padding: 1em;
text-align: center;
text-decoration: none;
[data-animation] & {
@media (prefers-reduced-motion: no-preference) {
transition: 600ms;
}
}
&:hover {
background-color: var(--tablist-color-interactive);
}
&[aria-selected="true"] {
background-color: var(--tablist-color-selected);
color: var(--tablist-color-active);
}
}
// Tab Panel
&__panel {
background-color: var(--tablist-color-selected);
overflow: hidden;
padding: 1em;
position: relative;
[data-fixed-height] & {
max-height: calc(400em/16);
overflow-y: auto;
overscroll-behavior-y: contain;
scrollbar-gutter: stable;
// Fallback Scrollbar
scrollbar-color: var(--tablist-color-active) var(--tablist-color-selected);
scrollbar-width: thin;
@supports #{"\selector(::-webkit-scrollbar)"} {
scrollbar-color: initial;
scrollbar-width: auto;
padding-inline-end: 0.5em;
&::-webkit-scrollbar {
width: calc(18rem / 16);
}
&::-webkit-scrollbar-track {
background-color: var(--tablist-color-selected);
margin-block: 0.5em;
}
&::-webkit-scrollbar-thumb {
background-color: var(--tablist-color-active);
border: calc(6rem / 16) var(--tablist-color-selected) solid;
border-radius: 100vw;
min-height: calc(100rem / 16);
}
}
}
}
// Tab Content
&__content {
position: relative;
// Your specific styles, if any.
[data-animation] & {
:not([hidden]) & {
inset-block-start: 0;
opacity: 1;
@starting-style {
inset-block-start: 3em;
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
transition: inset 600ms, opacity 300ms;
}
}
}
}
// Focus State
// Accessibility: Never remove focus outlines! Either redesign the outline or design an alternative focus state.
// However, a 3:1 ratio will be required. Learn: https://webaim.org/articles/contrast/
&__tab, &__panel {
&:focus {
outline-offset: -6px;
}
}
}/*!
Radancy Component Library: TabList
Contributor(s):
Michael "Spell" Spellacy
*/
(() => {
"use strict";
const initTablist = () => {
// Classes, data attributes, states, and strings.
const tabListClass = ".tablist";
const tabListListClass = ".tablist__list";
const tabListClassName = "tablist__tab";
const tabListTabClass = `.${tabListClassName}`;
const tabListDataDisableAnchor = "data-disable-anchor";
const tabListDataVertical = "data-vertical";
const tabListPanelClass = ".tablist__panel";
const tabLists = document.querySelectorAll(tabListClass);
const URLFragment = location.hash.slice(1);
// Display which version is in use via console:
console.log("%cTabList%cv2.0.0", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");
// Set up all Tablists on page
tabLists.forEach((list, e) => {
const index = e + 1;
list.setAttribute("id", `tablist-${index}`);
const listContainer = list.querySelector(tabListListClass);
listContainer.setAttribute("role", "tablist");
if (list.hasAttribute(tabListDataVertical)) {
listContainer.setAttribute("aria-orientation", "vertical");
}
// Get all tabs within tablist.
const tabs = list.querySelectorAll(tabListTabClass);
// Get all tabpanels within tablist.
const panels = list.querySelectorAll(tabListPanelClass);
// Array used to hold all panel IDs.
const panelNames = [];
// Prep each tab.
tabs.forEach((tab) => {
const tabID = tab.getAttribute("href").replace("#", "");
// Push all panel ID's to array.
panelNames.push(tabID);
// Setup each tab.
tab.parentElement.setAttribute("role", "presentation");
tab.setAttribute("aria-controls", tabID);
tab.setAttribute("id", `tab-${tabID}`);
tab.setAttribute("role", "tab");
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
// Highlight tab based on hash.
if (tabID === URLFragment) {
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
}
tab.addEventListener("click", (e) => {
const panelID = tab.getAttribute("href").replace("#", "");
tabs.forEach((tab) => {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide any open panels
const panels = tab.closest(tabListClass).querySelectorAll(tabListPanelClass);
const panelTarget = tab.closest(tabListClass).querySelector(`#${panelID}`);
panels.forEach((panel) => {
panel.setAttribute("hidden", "");
panel.removeAttribute("tabindex");
});
// Show selected panel
panelTarget.removeAttribute("hidden");
panelTarget.setAttribute("tabindex", "0");
// Highlight selected tab
tab.setAttribute("aria-selected", "true");
tab.removeAttribute("tabindex");
if (!tab.closest(tabListClass).hasAttribute(tabListDataDisableAnchor)) {
history.pushState(null, null, `#${tabID}`);
}
e.preventDefault();
});
// Get tab links used in tablist.
const tabLinks = list.querySelector(tabListListClass).querySelectorAll(tabListTabClass);
tab.addEventListener("keydown", (e) => {
// Check if the key pressed is an arrow key or Home/End
if (["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft", "Home", "End"].includes(e.key)) {
e.preventDefault(); // Prevent default scrolling behavior when arrows keys clicked.
// Get the index of the currently focused link
let focusedIndex = Array.from(tabLinks).indexOf(document.activeElement);
// Calculate the new index based on the arrow key pressed
if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
focusedIndex = (focusedIndex - 1 + tabLinks.length) % tabLinks.length; // Move left
} else if (["ArrowRight", "ArrowDown"].includes(e.key)) {
focusedIndex = (focusedIndex + 1) % tabLinks.length; // Move right
} else if (e.key === "Home") {
focusedIndex = 0; // Move to first tab
} else if (e.key === "End") {
focusedIndex = tabLinks.length - 1; // Move to last tab
}
// Focus and trigger event
tabLinks[focusedIndex].focus();
tabLinks[focusedIndex].click();
}
});
});
panels.forEach((panel) => {
const panelID = panel.getAttribute("id");
panel.setAttribute("aria-labelledby", `tab-${panelID}`);
panel.setAttribute("role", "tabpanel");
panel.setAttribute("tabindex", "0");
panel.setAttribute("hidden", "");
if (panelID === URLFragment) {
panel.removeAttribute("hidden");
}
});
if (!URLFragment || !panelNames.includes(URLFragment)) {
tabs[0].setAttribute("aria-selected", "true");
tabs[0].removeAttribute("tabindex");
panels[0].removeAttribute("hidden");
panels[0].setAttribute("tabindex", "0");
}
});
// All TabList panels must have unique IDs. Check to see if duplicate ID's exist and alert developer.
// Note: This function may not be needed. Instead, simply having functionality so dependent on ID, may make this moot.
const allPanelID = [];
const tabPanels = document.querySelectorAll(tabListPanelClass);
tabPanels.forEach((panel) => {
const panelID = panel.id;
allPanelID.push(panelID);
});
allPanelID.forEach((element, index, array) => {
// Check if the current element is equal to any subsequent element
array.slice(index + 1).forEach((nextElement) => {
if (element === nextElement) {
console.log(`%c Warning: Duplicate Tablist ID found: #${element}. Tab panels must only contain unique ID values. `, "background: #ff0000; color: #fff");
}
});
});
}
initTablist();
})();Properties Anchor (Properties)
Use the following properties to configure this component.
| Attribute | Applies To | Note | Description |
|---|---|---|---|
data-animation |
.tablist |
Boolean, Optional | Used to enable content to be animated when opened. Please note that the default animation leverages @starting-style at-rule. |
data-disable-anchor |
.tablist |
Boolean, Optional | Used to disable the anchor that appears in the address bar when a tab is clicked on. Do note that including an anchor can be useful for tracking and sharing specific content, so we should only remove when we have a good reason. |
data-fixed-height |
.tablist |
Boolean, Optional | Used to fix the tab content to a specific height. The height may be adusted in the CSS. |
data-scroll |
.tablist |
Boolean, Optional | Used to change the tabs into a scroll in smaller viewports. While scrolling is always an option, it should only be done sparingly. Allowing tabs to stack is always the better choice. |
data-stack |
.tablist |
Boolean, Optional | Used to allow the tabs to stack in smaller viewports. The default behavior of tabs are to wrap. |
data-vertical |
.tablist |
Boolean, Optional | Used to change the layout of the tabs from horizontal (default) to vertical. The CSS for this layout is tied to aria-orientation, which does not have wide assisitve technology support yet. |
| Class | Applies To | Note | Description |
|---|---|---|---|
.tablist |
div |
Mandatory | Used on the parent element that wraps your list and panels. |
.tablist__list |
ul |
Mandatory | Used on the ul element for each list. Must be a child of .tablist. |
.tablist__tab |
a |
Mandatory | Used on the a element for each tab. Must be a child of .tablist__list. |
.tablist__panel |
div |
Mandatory | Used on the parent element that wraps your content. Must be a child of .tablist. |
.tablist__content |
div |
Optional | Used on the element that directly wraps your content. Must be a child of .tablist__panel. |
Release Notes Anchor (Release Notes)
Keep up to date on major changes to this component.
- Feature
- March 30, 2026: Added support for Home and End. When either key is pressed within the component, focus will move to the first or last tab, respectively.
- Note
- April 24, 2025: Upgraded component to ES6. Thanks, Donte! Added gap to tabs to make them look more distinct from accordion. Tabs should look like tabs.
Report Issues
Find a bug? Want a new feature? Report it on JIRA. You're so awesome!