Skip to main content

A simple folding FAQ using a little JavaScript

- (incept: )

New version available: FAQs utilising ARIA

Dramatically updated and revamped to ease your workflow process.
Feedback & feature requests welcomed.

A simple folding FAQ

author: mike foskett incept: 20th April 2012

last update: 2nd August 2017

Turns a normal definition list (DL) into a folded question / answer FAQ.

Uses JavaScript to add interactivity and CSS to control the look and feel. Without JavaScript present the full DL is shown.


Q. Pre-applying a class of "on" to a DT forces it to show on page load.
It is recommended to leave the first question open to provide a hint to the user about the functionality presented.
Q. Will the code handle more than one sibling DD?
Apparently it will.
There is no limit to the number of sibling DDs that may be hidden.
An 'illegal' DT without a sibling DD. Note the icon space is reserved but no icon is displayed.
Q. Are multiple DL lists on one page supported?
Yes. As many as you need.
Just add a class of "faq" to the DL if you want to fold its DD content.
Q. Can I add a custom class name to the activating links?
Yes. Add the parameter linkClass : "customClassName" to the function call. Where customClassName may be any class name you wish.
By default no class name is added to the link.
Q. What parameters may be user defined?
The three class names used may be redefined by the user:
  • faqClass : "faq" - class name of the DLs which require content folding. The default is "faq".
  • onClass : "on" - script applied class to both the DT and DDs to show. The default is "on". It also switches the DT icon state.
  • linkClass : "" - optional class may be added to the DT activating link. Default none.
Call like so:
FAQ.init({ faqClass : "faq", onClass : "on", linkClass : "" });
To use with just the defaults FAQ.init({}); is enough. Remember if you change the default class names then it needs to be reflected in the HTML and CSS.
All aspects of look and feel are maintained by the CSS and hence user defined.
Q. What's with the icon graphic and can I use it?
The arrow graphic used is embedded in the CSS as a dataURI base 64 image with a back-up PNG for IEv6 & 7.
Feel free to use it as you wish.
Q. Is there anything else should I consider?
Ensure the activating link colours used are different to normal links. Helping the user to understand these links are a little different.

A complete commented bare bones demo version is available. The JavaScript is also available as a very light weight minified version [1.27Kb].

How does it work?

The script looks for all definition lists (DL) with a class name of "faq". Within each it finds the definition titles (DT) and checks for the presence of sibling definition data elements (DD). If a sibling DD is present the contents of the DT are placed inside a link.

An empty I tag is also added to the DT link for the icon, but if there isn't an associated DD then the link is omitted and a B tag is added.

If the DT already has a class="on" in the HTML then the same class is added to its DD siblings.

The "on" class allows for full control via CSS. Upon clicking the DT activating link the "on" class is toggled on both the DT and DDs.


The content is not hidden from screen-readers, it is shifted off-screen so it still appears in the document flow. The addition of the DT link ensures keyboard only users may also focus and activate the control.


Standard semantic XHTML definition list pattern. A class="faq" added to the DL so the JavaScript knows to act on it. A DT class="on" added to force the first DT/DD to show.

<dl class="faq">

  <dt class="on">Q. Pre-applying a class of "on" to a DT forces it to show on page load.</dt>
  <dd>It is recommended to leave the first question open to provide a hint to the user about the functionality presented.<br />Please ensure the link colours used are different to standard links too.</dd>

  <dt>Q. Will the code handle more than one sibling DD?</dt>
  <dd>Apparently it does.</dd>
  <dd>There is no limit to the number of sibling DDs that may be hidden.</dd>



All visual aspects are controlled by the CSS.

/* FAQ styles */
.faq dt {margin:2em 0 0.5em 0; font-weight:bold;}
.faq dd {margin-top:0.5em; margin-bottom:1em;}

/* if JavaScript is available move the DDs off screen */
.hasJS .faq dd {position:absolute; top:0; left:-200em; width:58em}

/* When .on is applied to the DD move it back on screen */
.hasJS .faq dd.on {position:static; left:0; width:auto}

/* Make sure the DT activating links look different to the pages normal links */
.faq dt a,
.faq dt a:visited {color:#737373; text-decoration:none}
.faq dt a:hover,
.faq dt a:focus,
.faq dt a:active {color:#000;}

/* An empty <i> tag is used for the icon.
   Note Base64 embedded sprite image is used with a back-up PNG available for IEv6 & 7.
   An empty <b> tag is used if the DT has no sibling DDs. */
.faq dt i,
.faq dt b {
  margin-right:1em; float:left; width: 16px; height: 17px; overflow:hidden; background-repeat: no-repeat;
  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAiBAMAAACkb0T0AAAAMFBMVEX///+kpKT4+Ph7e3vMzMyUlJTu7u68vLyEhIRzc3Pf39+NjY2ZmZnExMTU1NTq6uqVHy7CAAAAAXRSTlMAQObYZgAAAJRJREFUeF49kLENQjEQQ60I8RFSdohgikxCwQSMkA7RUaFfIOEtaCgoKRmBDViBjrvL/3b15OQS+yAtX6XDiieDVLDlzazvEQualeoIfMxK9QpksxzCQsBA7s4OaCQDcp0A74D1/vEMOJAM+AWMPu26ANi4ikBHuqxxPagvHDI7KIaCtSnqMIdvvY4KzpXvWoLWIv0BoKJU4+j6VIYAAAAASUVORK5CYII=);
.faq .on i {background-position:bottom;}
.faq dt b {background:none;}

A little CSS3 to smooth the transition:

.hasJS .faq dd {
  -moz-transition: opacity 0.3s ease-in;
  -o-transition: opacity 0.3s ease-in;
  -webkit-transition: opacity 0.3s ease-in;
  transition: opacity 0.3s ease-in;
.hasJS .faq dd.on {
  opacity: 1;

The JavaScript

The hasJS script is required to allow the CSS to act on the page prior to page load. It should be placed immediately after the page title.


The FAQ script is in a closure to prevent contaminating other variables.

var FAQ = (function () {

  var faqClass,
    faqDLs = [],

  // functions to test add or remove an objects class
  function hasClass(o,c){return new RegExp('(\\s|^)'+c+'(\\s|$)').test(o.className);}
  function addClass(o,c){if(!hasClass(o,c)){o.className+=' '+c;}}
  function removeClass(o,c){if(hasClass(o,c)){o.className=o.className.replace(new RegExp('(\\s|^)'+c+'(\\s|$)'),' ').replace(/\s+/g,' ').replace(/^\s|\s$/,'');}}

  // Cross-browser solution to get the next sibling object
  function getNextSibling(o) {
    var n = o.nextSibling;
    while (n !== null && n.nodeType !== 1) {
      n = n.nextSibling;
    return n;

The function showHideDDs() is called when the DT link is clicked. It toggles the "on" class both on itself and its sibling DD(s).

  function showHideDDs() {
    var dds = this.siblings,
      i = dds.length,
      p = this.parentNode;
    if (hasClass(p, onClass)) { //remove "on" class
      removeClass(p, onClass);
      while (i--) {
        removeClass(dds[i], onClass);
    } else { // add "on" class
      addClass(p, onClass);
      while (i--) {
        addClass(dds[i], onClass);
    return false;

Initialise the function

  function init(cfg) {

    // Over-ride the default class names if required
    faqClass = cfg.faqClass || "faq";
    onClass = cfg.onClass || "on";
    lnkClass = cfg.linkClass || "";

    // Create a list of all DLs identified by faqClass
    DLs = document.getElementsByTagName("dl");
    i = DLs.length;
    while (i--) {
      if (hasClass(DLs[i], faqClass)) {

    // now with each DL in the list
    i = faqDLs.length;
    while (i--) {

      // Loop through each DT
      dts = faqDLs[i].getElementsByTagName('dt');
      ii = dts.length;
      while (ii--) {

        // Create an activating link object
        lnkObj = document.createElement('a');

        // Get a list of DT siblings and attach to the link object
        lnkObj.siblings = [];
        ns = getNextSibling(dts[ii]);  // The DT's first sibling if it exists

        while (ns !== null) {

          // We are only interested if it is a DD
          if (ns.tagName === "DD") {
            ns = getNextSibling(ns);
          } else {

            // The next sibling is not a DD therefore end the loop
            ns = null;

        // Only add link object and associated html changes if there is at least one DD
        iii = lnkObj.siblings.length;
        if (iii) {

          // Store original DT content and then empty.
          lnkTxt = dts[ii].innerHTML;
          dts[ii].innerHTML = "";

          // If required add a class to the DT activating link.
          if (lnkClass !== "") {
            lnkObj.className = lnkClass;

          // Add click functionality.
          lnkObj.href = "#";
          lnkObj.onclick = showHideDDs;

          // Insert an i element to display a show/hide icon.

          // Copy original DT content back inside the new link.

          // Append the link to the empty DT.

          // If the DT has an "on" class stated, then add the same class to its DD siblings.
          if (hasClass(dts[ii], onClass)) {
            while (iii--) {
              addClass(lnkObj.siblings[iii], onClass);
        } else {

          // Otherwise add a B element to reserve icon space.
          dts[ii].innerHTML = "<b></b>" + dts[ii].innerHTML;

  return {
    init : init


Calling the function with default class names:


Alternatively call the function stating custom class names:

  faqClass : "yourFAQclass",  // Default "faq" - the DL class name
  onClass : "yourONclass",    // Default "on" used to show dd and switch dt icon state.
  linkClass : "yourLINKclass" // Default none - the DT link class name

Any issues please email me