HubSpot CMS

Accessible FAQ Module

An accessibility friendly FAQ/Accordion module that requires no CSS or JS to function. Easily add styling. Easily link to it within your pages.

Unverified
?

After receiving a minimum of 25 recommendations, an entry earns the "Community Approved" badge.

Please keep in mind, all entries are community created and may not be fully supported by HubSpot.

4 Recommendations

View on:

GitHub

HTML + HUBL
<section class="a11y-faq-module" id="faq" aria-label="Accordion List">
  <div class="page-center">

    {% for item in module.item %}
    <details class="a11y-faq-module__block">
      <summary class="a11y-faq-module__summary">
      {{ item.question }}
      </summary>
      <div class="a11y-faq-module__content">
        {% inline_rich_text field="details" value="{{ item.details }}" %}
      </div>

    </details>
    {% endfor %}

  </div>
</section>
          
        
CSS
          
            
          
        
JS
          
            /*
Optional Polyfill to add compatibility back to IE 11, fallsback uncollapsed without arrow - so this is up to you if you actually need it. No other JS.
Details Element Polyfill 2.3.1
Copyright © 2019 Javan Makhmali
 */
(function() {
    "use strict";
    var element = document.createElement("details");
    element.innerHTML = "<summary>a</summary>b";
    element.setAttribute("style", "position: absolute; left: -9999px");
    var support = {
      open: "open" in element && elementExpands(),
      toggle: "ontoggle" in element
    };
    function elementExpands() {
      (document.body || document.documentElement).appendChild(element);
      var closedHeight = element.offsetHeight;
      element.open = true;
      var openedHeight = element.offsetHeight;
      element.parentNode.removeChild(element);
      return closedHeight != openedHeight;
    }
    var styles = '\ndetails, summary {\n  display: block;\n}\ndetails:not([open]) > *:not(summary) {\n  display: none;\n}\nsummary::before {\n  content: "►";\n  padding-right: 0.3rem;\n  font-size: 0.6rem;\n  cursor: default;\n}\n[open] > summary::before {\n  content: "▼";\n}\n';
    var _ref = [], forEach = _ref.forEach, slice = _ref.slice;
    if (!support.open) {
      polyfillStyles();
      polyfillProperties();
      polyfillToggle();
      polyfillAccessibility();
    }
    if (support.open && !support.toggle) {
      polyfillToggleEvent();
    }
    function polyfillStyles() {
      document.head.insertAdjacentHTML("afterbegin", "<style>" + styles + "</style>");
    }
    function polyfillProperties() {
      var prototype = document.createElement("details").constructor.prototype;
      var setAttribute = prototype.setAttribute, removeAttribute = prototype.removeAttribute;
      var open = Object.getOwnPropertyDescriptor(prototype, "open");
      Object.defineProperties(prototype, {
        open: {
          get: function get() {
            if (this.tagName == "DETAILS") {
              return this.hasAttribute("open");
            } else {
              if (open && open.get) {
                return open.get.call(this);
              }
            }
          },
          set: function set(value) {
            if (this.tagName == "DETAILS") {
              return value ? this.setAttribute("open", "") : this.removeAttribute("open");
            } else {
              if (open && open.set) {
                return open.set.call(this, value);
              }
            }
          }
        },
        setAttribute: {
          value: function value(name, _value) {
            var _this = this;
            var call = function call() {
              return setAttribute.call(_this, name, _value);
            };
            if (name == "open" && this.tagName == "DETAILS") {
              var wasOpen = this.hasAttribute("open");
              var result = call();
              if (!wasOpen) {
                var summary = this.querySelector("summary");
                if (summary) summary.setAttribute("aria-expanded", true);
                triggerToggle(this);
              }
              return result;
            }
            return call();
          }
        },
        removeAttribute: {
          value: function value(name) {
            var _this2 = this;
            var call = function call() {
              return removeAttribute.call(_this2, name);
            };
            if (name == "open" && this.tagName == "DETAILS") {
              var wasOpen = this.hasAttribute("open");
              var result = call();
              if (wasOpen) {
                var summary = this.querySelector("summary");
                if (summary) summary.setAttribute("aria-expanded", false);
                triggerToggle(this);
              }
              return result;
            }
            return call();
          }
        }
      });
    }
    function polyfillToggle() {
      onTogglingTrigger(function(element) {
        element.hasAttribute("open") ? element.removeAttribute("open") : element.setAttribute("open", "");
      });
    }
    function polyfillToggleEvent() {
      if (window.MutationObserver) {
        new MutationObserver(function(mutations) {
          forEach.call(mutations, function(mutation) {
            var target = mutation.target, attributeName = mutation.attributeName;
            if (target.tagName == "DETAILS" && attributeName == "open") {
              triggerToggle(target);
            }
          });
        }).observe(document.documentElement, {
          attributes: true,
          subtree: true
        });
      } else {
        onTogglingTrigger(function(element) {
          var wasOpen = element.getAttribute("open");
          setTimeout(function() {
            var isOpen = element.getAttribute("open");
            if (wasOpen != isOpen) {
              triggerToggle(element);
            }
          }, 1);
        });
      }
    }
    function polyfillAccessibility() {
      setAccessibilityAttributes(document);
      if (window.MutationObserver) {
        new MutationObserver(function(mutations) {
          forEach.call(mutations, function(mutation) {
            forEach.call(mutation.addedNodes, setAccessibilityAttributes);
          });
        }).observe(document.documentElement, {
          subtree: true,
          childList: true
        });
      } else {
        document.addEventListener("DOMNodeInserted", function(event) {
          setAccessibilityAttributes(event.target);
        });
      }
    }
    function setAccessibilityAttributes(root) {
      findElementsWithTagName(root, "SUMMARY").forEach(function(summary) {
        var details = findClosestElementWithTagName(summary, "DETAILS");
        summary.setAttribute("aria-expanded", details.hasAttribute("open"));
        if (!summary.hasAttribute("tabindex")) summary.setAttribute("tabindex", "0");
        if (!summary.hasAttribute("role")) summary.setAttribute("role", "button");
      });
    }
    function eventIsSignificant(event) {
      return !(event.defaultPrevented || event.ctrlKey || event.metaKey || event.shiftKey || event.target.isContentEditable);
    }
    function onTogglingTrigger(callback) {
      addEventListener("click", function(event) {
        if (eventIsSignificant(event)) {
          if (event.which <= 1) {
            var element = findClosestElementWithTagName(event.target, "SUMMARY");
            if (element && element.parentNode && element.parentNode.tagName == "DETAILS") {
              callback(element.parentNode);
            }
          }
        }
      }, false);
      addEventListener("keydown", function(event) {
        if (eventIsSignificant(event)) {
          if (event.keyCode == 13 || event.keyCode == 32) {
            var element = findClosestElementWithTagName(event.target, "SUMMARY");
            if (element && element.parentNode && element.parentNode.tagName == "DETAILS") {
              callback(element.parentNode);
              event.preventDefault();
            }
          }
        }
      }, false);
    }
    function triggerToggle(element) {
      var event = document.createEvent("Event");
      event.initEvent("toggle", false, false);
      element.dispatchEvent(event);
    }
    function findElementsWithTagName(root, tagName) {
      return (root.tagName == tagName ? [ root ] : []).concat(typeof root.getElementsByTagName == "function" ? slice.call(root.getElementsByTagName(tagName)) : []);
    }
    function findClosestElementWithTagName(element, tagName) {
      if (typeof element.closest == "function") {
        return element.closest(tagName);
      } else {
        while (element) {
          if (element.tagName == tagName) {
            return element;
          } else {
            element = element.parentNode;
          }
        }
      }
    }
  })();
  
          
        
Have Questions?

Ask technical questions in Slack.

HubSpot Developer Slack

Not a member yet? - join here
READ ME (VIEW IN GITHUB)

HS-Accessible-FAQ-Module

An accessibility friendly FAQ HubSpot Custom Module that requires no CSS or JS to function. animation of FAQ Toggle

Demo of the output of the module: https://codepen.io/thewebtech/pen/xevLpE

JS Pane JS is an optional polyfill, this fallsback gracefully without the polyfill, just included it as some may want it. https://caniuse.com/#search=details

Super easy to add your own styling, class names are based on BEM.

Wrapper - .a11y-faq-module

Q&A block - .a11y-faq-module__block

Question text -.a11y-faq-module__summary

Answer text - .a11y-faq-module__content

To link to the FAQ section from anywhere in your page <a href="#faq">FAQs</a>

 
4 Recommendations

Jon McLaren

@HubSpot

Web Developer, HubSpot Development Specialist

View Jon McLaren’s Gallery (4 Entries)

Other Open Source Projects

Browse all other open source projects

CrankShaft Framework

A modern framework for accelerating build times on the HubSpot CMS. Based on a modified Bootstrap 4 framework.

Lead developers: Jon McLaren

Developer Chrome Extension

Chrome/Chromium extension for HubSpot CMS Developers that adds a developer menu, dark theme and useful shortcuts to commonly used HubSpot query parameters, resources, and tools for making HubSpot Development easier and more enjoyable.

Lead developers: Jon McLaren , William Spiro , Gonzalo Torreras

VS Code HubL Language Extension

This extension enables super fast local development of CMS pages, and is a great compliment to using the new local HubL server. It contains comprehensive HubL tag, function, filter and expression test auto-complete snippets, as well as their documentation.

Lead developers: William Spiro

Not coding on HubSpot CMS yet?

We invite you to explore why thousands of developers LOVE coding with HubSpot!

Contribute
Made with  by community members: InboundLabs.co