Hi Matt,
The Google sheet would probably be a helpful starting point, but Loops & Logic should absolutely be able to do what you need, especially easily if all of your data is stored in ACF fields and you aren’t mixing in data from Woocommerce product attributes, shipping info and so on (though L&L could accommodate this too). Our ACF documentation can be found here and should cover everything you need to get the field values:
For your accordions, we do have a demo in our Snippet library you could use as a starting point:
Here’s the HTML I usually use for A11y-friendly tabs:
<div class="tabs">
<div role="tablist" aria-label="Tablist Accessibility Title" class="card tabs__control-bar">
<button role="tab" aria-selected="true" aria-controls="tabSection_1" id="tab_1">
Tab 1
</button>
<button role="tab" aria-selected="false" aria-controls="tabSection_2" id="tab_2" tabindex="-1">
</div>
<div tabindex="0" class="tabs__panel" role="tabpanel" id="tabSection_1" aria-labelledby="tab_1">
Tab 1 Content Here
</div>
<div tabindex="0" class="tabs__panel" role="tabpanel" id="tabSection_2" aria-labelledby="tab_2" hidden="hidden">
Tab 2 Content here
</div>
</div>
When using it with L&L I tend to make it a bit more dynamic:
Template
<Set label>Tabs</Set> <!-- Replace with an accessibility label for the tablist-->
<List name=visible_tabs> <!-- One <Item> per tag containing your tab titles. This structure can be handy if you need to dynamically hide a tab, just wrap the <Item> in an <If> statement -->
<Item>Info</Item> <!-- Replace with your Tab 1 Title -->
<Item>Specifications</Item> <!-- Replace with your Tab 2 Title -->
<!-- Add additional items as necessary -->
</List>
<Set tabcontent_1> <!-- Replace with your Tab 1 Content-->
Tab 1 Content Here
</Set>
<Set tabcontent_2> <!-- Replace with your Tab 2 Content-->
Tab 2 Content Here
</Set>
<!-- Add additional items as necessary, respecting naming convention of "tabcontent_" followed by a number matching the item number from your list -->
<div class="tabs">
<div role="tablist" aria-label="{Get label}" class="card tabs__control-bar">
<Loop list=visible_tabs>
<Set tab_count><Get loop=count /></Set>
<button role="tab" aria-selected="{If variable=tab_count value=1}true{Else /}false{/If}" aria-controls="tabSection_{Get tab_count}" id="tab_{Get tab_count}" tag-attributes="{If variable=tab_count not value=1}tabindex='-1'{/If}">
<Field />
</button>
</Loop>
</div>
<Loop list=visible_tabs>
<Set tabpanel_count><Get loop=count /></Set>
<div tabindex="0" class="tabs__panel" role="tabpanel" id="tabSection_{Get tabpanel_count}" aria-labelledby="tab_{Get tabpanel_count}">
<Get name="tabcontent_{Get tabpanel_count}" />
</div>
</Loop>
</div>
Here’s the basic Styles and Scripts you’ll need to get the tabs working:
CSS
.tabs__control-bar {
display:flex;
justify-content:center;
position:relative;
flex-wrap:wrap;
overflow:unset
}
.tabs__control-bar button {
font-size:1rem;
padding:1.5em 1em;
display:inline-flex;
margin:0
}
@media (max-width:991px) {
.tabs__control-bar {
justify-content:flex-start
}
.tabs__control-bar button {
min-width:max-content;
font-size:14px;
padding:1.25em 0.5em
}
}
.tabs__control-bar button {
cursor:pointer;
}
.tabs__control-bar button[aria-selected="true"] {
box-shadow:inset 0 -3px 0
}
.tabs__panel {
padding-top:1rem
}
Javascript
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/
(function () {
var tablist = document.querySelectorAll('.tabs__control-bar')[0];
var tabs;
var panels;
generateArrays();
function generateArrays() {
tabs = tablist.querySelectorAll('[role="tab"]');
panels = document.querySelectorAll('.tabs__panel');
}
// For easy reference
var keys = {
end: 35,
home: 36,
left: 37,
up: 38,
right: 39,
down: 40,
keydelete: 46,
enter: 13,
space: 32
};
// Add or subtract depending on key pressed
var direction = {
37: -1,
38: -1,
39: 1,
40: 1
};
// Bind listeners
for (i = 0; i < tabs.length; ++i) {
addListeners(i);
};
// Check URL for tab ID
tabs.forEach(function(element) {
if (window.location.hash === '#' + element.id.replace('courseTab_', '')) {
activateTab(element, false);
}
});
function addListeners (index) {
tabs[index].addEventListener('click', clickEventListener);
tabs[index].addEventListener('keydown', keydownEventListener);
tabs[index].addEventListener('keyup', keyupEventListener);
// Build an array with all tabs (<button>s) in it
tabs[index].index = index;
};
// When a tab is clicked, activateTab is fired to activate it
function clickEventListener (event) {
var tab = event.target;
activateTab(tab, false);
};
// Handle keydown on tabs
function keydownEventListener (event) {
var key = event.keyCode;
switch (key) {
case keys.end:
event.preventDefault();
// Activate last tab
focusLastTab();
break;
case keys.home:
event.preventDefault();
// Activate first tab
focusFirstTab();
break;
// Up and down are in keydown
// because we need to prevent page scroll >:)
case keys.up:
case keys.down:
determineOrientation(event);
break;
};
};
// Handle keyup on tabs
function keyupEventListener (event) {
var key = event.keyCode;
switch (key) {
case keys.left:
case keys.right:
determineOrientation(event);
break;
case keys.keydelete:
determineDeletable(event);
break;
case keys.enter:
case keys.space:
activateTab(event.target);
break;
};
};
// When a tablist's aria-orientation is set to vertical,
// only up and down arrow should function.
// In all other cases only left and right arrow function.
function determineOrientation (event) {
var key = event.keyCode;
var vertical = tablist.getAttribute('aria-orientation') == 'vertical';
var proceed = false;
if (vertical) {
if (key === keys.up || key === keys.down) {
event.preventDefault();
proceed = true;
};
}
else {
if (key === keys.left || key === keys.right) {
proceed = true;
};
};
if (proceed) {
switchTabOnArrowPress(event);
};
};
// Either focus the next, previous, first, or last tab
// depending on key pressed
function switchTabOnArrowPress (event) {
var pressed = event.keyCode;
if (direction[pressed]) {
var target = event.target;
if (target.index !== undefined) {
if (tabs[target.index + direction[pressed]]) {
tabs[target.index + direction[pressed]].focus();
}
else if (pressed === keys.left || pressed === keys.up) {
focusLastTab();
}
else if (pressed === keys.right || pressed == keys.down) {
focusFirstTab();
};
};
};
};
// Activates any given tab panel
function activateTab (tab, setFocus) {
setFocus = setFocus || true;
// Deactivate all other tabs
deactivateTabs();
// Remove tabindex attribute
tab.removeAttribute('tabindex');
// Set the tab as selected
tab.setAttribute('aria-selected', 'true');
// Get the value of aria-controls (which is an ID)
var controls = tab.getAttribute('aria-controls');
// Remove hidden attribute from tab panel to make it visible
document.getElementById(controls).removeAttribute('hidden');
// Add URL hash if not already there
if (window.location.hash !== '#' + tab.id.replace('courseTab_', '')) {
history.pushState({}, '', '#' + tab.id.replace('courseTab_', ''));
}
// Set focus when required
if (setFocus) {
tab.focus();
};
};
// Deactivate all tabs and tab panels
function deactivateTabs () {
for (t = 0; t < tabs.length; t++) {
tabs[t].setAttribute('tabindex', '-1');
tabs[t].setAttribute('aria-selected', 'false');
};
for (p = 0; p < panels.length; p++) {
panels[p].setAttribute('hidden', 'hidden');
};
};
// Make a guess
function focusFirstTab () {
tabs[0].focus();
};
// Make a guess
function focusLastTab () {
tabs[tabs.length - 1].focus();
};
// Detect if a tab is deletable
function determineDeletable (event) {
target = event.target;
if (target.getAttribute('data-deletable') !== null) {
// Delete target tab
deleteTab(event, target);
// Update arrays related to tabs widget
generateArrays();
// Activate the closest tab to the one that was just deleted
if (target.index - 1 < 0) {
activateTab(tabs[0]);
}
else {
activateTab(tabs[target.index - 1]);
};
};
};
// Deletes a tab and its panel
function deleteTab (event) {
var target = event.target;
var panel = document.getElementById(target.getAttribute('aria-controls'));
target.parentElement.removeChild(target);
panel.parentElement.removeChild(panel);
};
// Determine whether there should be a delay
// when user navigates with the arrow keys
function determineDelay () {
var hasDelay = tablist.hasAttribute('data-delay');
var delay = 0;
if (hasDelay) {
var delayValue = tablist.getAttribute('data-delay');
if (delayValue) {
delay = delayValue;
}
else {
// If no value is specified, default to 300ms
delay = 300;
};
};
return delay;
};
}());