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 test

View Jon McLaren’s Gallery (4 Entries)

Contribute to the Gallery!

We bet you’ve coded some amazing stuff. Showcase them and help the community grow.

Contribute

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