How to Display Product Attributes and ACF Product Specs in an Accordion Tab as Tables (Using Beaver Builder and Themer)

Hi everyone,

I’m working on a WooCommerce product page and need some guidance on displaying product attributes and specifications (stored as ACF fields) within a product tab. The goal is to organize this data into tables and display them within an accordion layout for a better user experience.

Here’s a summary of what I’m trying to achieve:

  • Display product attributes and ACF specifications as tables.
  • Group these tables under different accordion sections within a specifications product tab.
  • I’m using Beaver Builder and Beaver Themer, and I have access to the Beaver Builder Loops and Logic module to manage dynamic content.

Please see my screenshot below:

You can also visit the live page here:

I’m a bit stuck on how to properly structure the loop and conditional logic to pull in both the product attributes and ACF fields and then display them as tables inside the accordion. It would be incredibly helpful if anyone has done something similar or can provide a code snippet or step-by-step instructions.

Also, I am open to hiring someone to help me complete this, as I realize that it requires more advanced customization. Please reach out if you’re interested or if you have any recommendations.

Thanks in advance for your help!

Best regards,

Matt

Hi Matt, can you elaborate on how you want to display the specs? It will also nice if you can somehow give us some info on how you setup the ACF fields you need in the table of products tab. Will there also a different table for each accordion?

Hi Erica,

Thanks for your response! Let me clarify a bit further.

I want to display the specifications in a tabular format within each accordion section. Each accordion will have a different table, as the specifications are grouped into different field groups using ACF. These field groups represent different sets of product specifications so that each group will correspond to its own accordion section and I’ve already set up the ACF fields within these field groups. I can provide a Google Sheet with all the field names and their corresponding groups. This should help a developer understand what needs to be done and how the data is structured.

I’m a bit surprised that this wasn’t clear from my original post, but hopefully, this provides a better understanding of my goal. I’m looking to pull these grouped fields into the accordion sections as tables, and I believe the Loops and Logic module can help facilitate this dynamic content display.

Let me know if you need more details. I would appreciate any help or recommendations you can provide!

Best regards,

Matt

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;
  };
}());
1 Like

Julia,

Thank you so much for your reply.
I want to combine ACF fields with woo attributes in a table however, I am also using Beaver Builder and placing the loops and logic module in a tabbed module so my thought process it that all I need is the loops and logic snippet and CSS.

1 Like