An accessibility friendly FAQ/Accordion module that requires no CSS or JS to function. Easily add styling. Easily link to it within your pages.
Created by: Jon McLaren
Tags: Lists, Tabs and accordions
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.
View on:
<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>
/*
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;
}
}
}
}
})();
An accessibility friendly FAQ HubSpot Custom Module that requires no CSS or JS to function.
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>
A modern framework for accelerating build times on the HubSpot CMS. Based on a modified Bootstrap 4 framework.
Lead developers: Jon McLaren
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
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
Find an entry with an issue, bug, or abuse?
Please complete the below information in detail and we will investigate.
Reporting as Luke Summerfield