City of Ghent Style Guide

Accordion

When to use this component

An accordion component is used to:

  • Show an overview of sections of multiple, related sections of content, that can be read independently from each other and which users typically are not interested to see all, but rather one or a few specific sections.
  • Make it possible for users to show or hide those section as needed.
  • Hide a simple piece or section of content that is advanced or that is only necessary for a minority of users (progressive disclosure principle).

When not to use this component

  • Do not use an accordion component to hide content that is essential to all users.
  • Do not use an accordion component when the amount of content in the items (when expanded) will make the page too long or too slow to load. Only use accordion components for simple content.

Alternatives to using an accordion component:

  • Reduce and simplify the content so that it can be placed as is on the page, without hiding it or the need for an accordion component.
  • Split the content accross multiple pages, especially if the amount of content is large (more than one text paragraph or if also other components than text paragraphs are needed to convey the message).
  • Keep the content as is on the page, separate the content by headings.
  • Use a table of contents component to let users navigate quickly to specific sections of content.

How it works

There are two types of accordions:

  • Single expandable item
  • Multiple expandable items

Adding the class ‘dropdown’ will add the following behaviour: when you click on an element that isn’t a dropdown, all dropdowns will be closed. This behaviour is used in the case of an application that doesn’t refresh when clicking a button or link.

Single expandable item

Accordions with one single expandable item are displayed with a chevron on the right-hand side.

  • When collapsed, the chevron points down, to indicate that it can be expanded.
  • When expanded, the chevron points up, to indicate that it can be collapsed.

By default, the single expandable item is collapsed.

The single expandable items has a label that tells what content is in the item. Do not start the label with a verb that describes the collapsing or expanding. This is not necessary because the item is already in an accordion. For example, if the item is a list of opening hours deze week, don’t use a label “Show opening hours this week” but use “Opening hours this week” instead.

Multiple expandable items

Accordions with multiple expandable items are displayed in a list of items with labels with a plus or a minus icon on the left-hand side of each item.

  • When an item is collapsed, the plus icon is shown, to indicate that it can be expanded.
  • When an item is expanded, the minus icon is shown, to indicate that it can be collapsed.

By default, all the expandable items are collapsed. Multiple items can be expanded at the same time.

The expandable items each have a label that tells what content is in the item. Do not start the label with a verb that describes the collapsing or expanding. This is not necessary because the item is already in an accordion. For example, if an item is about transportation by train, don’t use a label “Show transportation by train” but use “Transportation by train” instead.

Markup

  • Multiple expandable items must always be contained in an unordered- or description list.
  • The toggle must be of type button and preferably wrapped in an element with (implicit) role header.
  • The toggle button has both aria-expanded to indicate it’s current state and aria-controls to indicate which element it controls.

Javascript

Usage

Create a new accordion object by running:

new Accordion(element);

Where element contains a button with:

  • default class: accordion--button
  • aria-expanded (true or false)
  • aria-controls, the unique ID of the collapsible element

And a collapsible element with:

  • default class: accordion--content

By default, the accordion will initiate automatically and hide or show the content according to the aria-expanded attribute.
If the hash in the URL contains a matching ID, this element will also be expanded.

For instance:

<div class="accordion">
    <h3>
        <button aria-controls="accordion--content-ID"
                aria-expanded="false"
                class="accordion--button">
            single accordion
        </button>
    </h3>
    <div class="accordion--content" id="accordion--content-ID">
        ...
    </div>
</div>

Options

Option Type Default Description
expand function function(button, content) Function triggered after the expanded class is added, the ‘hidden’ attribute is removed from the content and aria-hidden is set to false.
collapse function function(button, content) Function triggered after the expanded class is removed and aria-hidden is set to true. By default, the ‘hidden’ attribute is set in the transitionEnd function
transitionEnd function function(event) Triggered for each transitionEnd event, use this to add the ‘hidden’ attribute after the content has been transitioned out of view.
resizeEvent function function(event, expandedContent) ExpandedContent is an array containing all expanded elements. Use this to trigger the ‘expand’ function on window.resize
init Boolean true Set to false if you want to manually initiate the accordion object (object.init())
buttonSelector String 'button.accordion--button' QuerySelector to identify the accordion trigger button
accordionExpandedClass String 'accordion--expanded' Determine which class is added to the expanded content.

Functions

Function Description
init() Manually initiate the accordion, this will expand or collapse all content according to the aria-expanded state
closeAll() Close all collapsible content in this accordion
openAll() Open all collapsible content in this accordion
<!-- Default -->
<div class="accordion dropdown">
  <h3>
    <button aria-controls="accordion--single--content--1" aria-expanded="false" class="accordion--button">{{ buttonText }}</button>
  </h3>
  <div id="accordion--single--content--1" class="accordion--content">
    {{ content }}
  </div>
</div>

<!-- Multiple -->
<ul class="accordion">
  {% for item in items %}
    <li>
      <h3>
        <button class="accordion--button"
                aria-expanded="false"
                aria-controls="accordion--multiple--content--{{ loop.index }}">
          {{ item.buttonText }}
        </button>
      </h3>
      <div class="accordion--content"
           id="accordion--multiple--content--{{ loop.index }}">
        {{ item.content }}
      </div>
    </li>
  {% endfor %}
</ul>
<!-- Default -->
<div class="accordion dropdown">
    <h3>
        <button aria-controls="accordion--single--content--1" aria-expanded="false" class="accordion--button">single accordion</button>
    </h3>
    <div id="accordion--single--content--1" class="accordion--content">
        <p>Single item accordions are indicated by a chevron.</p>
        <p>Below is some nonsens text.</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
    </div>
</div>

<!-- Multiple -->
<ul class="accordion">
    <li>
        <h3>
            <button class="accordion--button" aria-expanded="false" aria-controls="accordion--multiple--content--1">
                First item
            </button>
        </h3>
        <div class="accordion--content" id="accordion--multiple--content--1">
            <p>Multiple item accordions are contained in a list element.</p>
            <p>They are indicated with a 'plus' or 'minus' icon</p>
            <p>Below is some nonsens text.</p>
            <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
        </div>
    </li>
    <li>
        <h3>
            <button class="accordion--button" aria-expanded="false" aria-controls="accordion--multiple--content--2">
                Second item
            </button>
        </h3>
        <div class="accordion--content" id="accordion--multiple--content--2">
            <p>Single item accordions are indicated by a chevron.</p>
            <p>Below is some nonsens text.</p>
            <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
        </div>
    </li>
    <li>
        <h3>
            <button class="accordion--button" aria-expanded="false" aria-controls="accordion--multiple--content--3">
                Third item
            </button>
        </h3>
        <div class="accordion--content" id="accordion--multiple--content--3">
            <p>Single item accordions are indicated by a chevron.</p>
            <p>Below is some nonsens text.</p>
            <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
        </div>
    </li>
    <li>
        <h3>
            <button class="accordion--button" aria-expanded="false" aria-controls="accordion--multiple--content--4">
                Fourth item
            </button>
        </h3>
        <div class="accordion--content" id="accordion--multiple--content--4">
            <p>Single item accordions are indicated by a chevron.</p>
            <p>Below is some nonsens text.</p>
            <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
        </div>
    </li>
</ul>

/* Default */
{
  "buttonText": "single accordion",
  "content": "<p>Single item accordions are indicated by a chevron.</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
}

/* Multiple */
{
  "items": [
    {
      "buttonText": "First item",
      "content": "<p>Multiple item accordions are contained in a list element.</p><p>They are indicated with a 'plus' or 'minus' icon</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
    },
    {
      "buttonText": "Second item",
      "content": "<p>Single item accordions are indicated by a chevron.</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
    },
    {
      "buttonText": "Third item",
      "content": "<p>Single item accordions are indicated by a chevron.</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
    },
    {
      "buttonText": "Fourth item",
      "content": "<p>Single item accordions are indicated by a chevron.</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
    }
  ]
}

  • Content:
    .accordion {
    
      .accordion--button {
        @include icon(chevron-down);
        position: relative;
        padding-right: 1.6rem;
        padding-left: 0;
        color: inherit;
        text-align: left;
    
        &::before {
          position: absolute;
          right: 0;
          transition: transform .2s ease-in-out;
          font-size: 1.4rem;
          line-height: 1.1;
        }
    
        &[aria-expanded=true] {
          &::before {
            transform: scaleY(-1);
          }
        }
      }
    
      .accordion--content {
        transition: max-height .5s ease-in-out;
        overflow: hidden;
    
        p {
          line-height: 1.75;
        }
      }
    
    }
    
    ul.accordion,
    dl.accordion {
    
      .accordion--button {
    
        width: 100%;
        padding: 0;
        overflow: visible;
    
        &::before {
          position: relative;
          right: auto;
          left: 0;
          min-width: 1.6rem;
          transition: transform .2s ease-in-out;
          font-size: 1.2rem;
          line-height: 1.2;
          text-align: left;
        }
    
        &[aria-expanded=true] {
          @include icon(minus);
    
          &::before {
            transform: none;
          }
        }
    
        &[aria-expanded=false] {
          @include icon(plus);
        }
      }
    
      .accordion--content {
        margin-left: 1.6rem;
      }
    }
    
    ul.accordion {
      margin: 0;
    
      >li {
        list-style: none;
      }
    }
    
    dl.accordion {
      dd {
        padding: 0;
      }
    }
    
  • URL: /components/raw/accordion/_accordion.scss
  • Filesystem Path: components/31-molecules/accordion/_accordion.scss
  • Size: 1.3 KB
  • Content:
    'use strict';
    
    (function () {
    
      if (!Accordion) { // eslint-disable-line no-undef
        return;
      }
    
      const selected = document.querySelectorAll('.accordion');
      for (let i = selected.length; i--;) {
        new Accordion(selected[i]); // eslint-disable-line no-undef
      }
    
    })();
    
  • URL: /components/raw/accordion/accordion.bindings.js
  • Filesystem Path: components/31-molecules/accordion/accordion.bindings.js
  • Size: 275 Bytes
  • Content:
    'use strict';
    
    /* global define, module */
    (function (root, factory) {
      if (typeof define === 'function' && define.amd) {
        define(factory);
      }
      else {
        if (typeof exports === 'object') {
          module.exports = factory();
        }
        else {
          root.Accordion = factory();
        }
      }
    }(this || window, function () {
    
      return function (elem, options) {
    
        let expandedContent = [];
    
        /**
         * Override default options with options param.
         */
        options = (() => {
          const defaults = {
            expand: (button, content) => {
              content.style.maxHeight = `${content.scrollHeight}px`;
            },
            collapse: (button, content) => {
              content.style.maxHeight = 0;
            },
            transitionEnd: (e) => {
              if (e.propertyName !== 'max-height') {
                return;
              }
              if (!e.currentTarget.classList.contains('accordion--expanded')) {
                e.currentTarget.setAttribute('hidden', 'hidden');
              }
            },
            resizeEvent: (e, expandedContent) => {
              for (let i = expandedContent.length; i--;) {
                options.expand(null, expandedContent[i]);
              }
            },
            init: true,
            buttonSelector: 'button.accordion--button',
            accordionExpandedClass: 'accordion--expanded'
          };
          const keys = Object.keys(defaults);
          options = options || {};
    
          for (let i = keys.length; i--;) {
            options[keys[i]] = options[keys[i]] || defaults[keys[i]];
          }
    
          return options;
        })();
    
        const buttons = elem.querySelectorAll(options.buttonSelector);
    
        /**
         * Toggle aria-expanded attributes and trigger visibility Change function.
         *
         * @param {event} e The triggered event.
         */
        const toggle = (e) => {
          e.preventDefault();
          const button = e.currentTarget;
          button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');
          setVisibility(button);
        };
    
        /**
         * Handle keyboard input: arrows, home & end.
         *
         * @param {event} e The triggered event.
         */
        const keyDown = (e) => {
          const keyCode = e.keyCode || e.which;
          let current = (() => {
            for (let i = buttons.length; i--;) {
              if (buttons[i] === e.currentTarget) {
                return i;
              }
            }
          })();
          switch (keyCode) {
            case 40: // arrow down
              e.preventDefault();
              if (current === buttons.length - 1) {
                current = -1;
              }
              buttons[current + 1].focus();
              break;
            case 38: // arrow up
              e.preventDefault();
              if (current === 0) {
                current = buttons.length;
              }
              buttons[current - 1].focus();
              break;
            case 36: // home
              e.preventDefault();
              buttons[0].focus();
              break;
            case 35: // end
              e.preventDefault();
              buttons[buttons.length - 1].focus();
              break;
          }
        };
    
        /**
         * Add events to buttons.
         * Listen for animationEnd on accordionContent.
         */
        const addEvents = () => {
          const onResize = (e) => {
            options.resizeEvent(e, expandedContent);
          };
    
          for (let i = buttons.length; i--;) {
            const button = buttons[i];
            button.addEventListener('click', toggle);
            button.addEventListener('keydown', keyDown);
    
            const accordionContent = elem.querySelector(`#${button.getAttribute('aria-controls')}`);
            accordionContent.addEventListener('transitionend', options.transitionEnd);
    
            if (options.resizeEvent) {
              window.addEventListener('resize', onResize);
            }
          }
    
          window.addEventListener('hashchange', hashEvent);
        };
    
        /**
         * Hide or show the accordion content.
         *
         * @param {Object} button  The accordion button.
         * @param {boolean|false} isInitial  True if this is the first run
         *   triggered by init().
         */
        const setVisibility = (button, isInitial) => {
    
          const accordionContent = elem.querySelector(`#${button.getAttribute('aria-controls')}`);
          if (!accordionContent) {
            return;
          }
    
          if (button.getAttribute('aria-expanded') === 'true') {
            accordionContent.classList.add(options.accordionExpandedClass);
            accordionContent.setAttribute('aria-hidden', 'false');
            accordionContent.removeAttribute('hidden');
            expandedContent.push(accordionContent);
            options.expand(button, accordionContent);
          }
          else {
            accordionContent.classList.remove(options.accordionExpandedClass);
            accordionContent.setAttribute('aria-hidden', 'true');
            if (isInitial) {
              accordionContent.setAttribute('hidden', 'hidden');
            }
            expandedContent.filter(content => content !== accordionContent);
            options.collapse(button, accordionContent);
          }
        };
    
        /**
         * Hide dropdowns when clicking outside the element.
         *
         * @param {Object} event  The js event.
         */
        window.onclick = function (event) {
          if (!event.target.matches('.dropdown button')) {
            var dropdowns = document.getElementsByClassName('dropdown');
            for (var i = 0; i < dropdowns.length; i++) {
              var button = dropdowns[i].querySelector('button');
    
              if (button.getAttribute('aria-expanded') === 'false') {
                return;
              }
    
              const accordionContent = dropdowns[i].querySelector(`#${button.getAttribute('aria-controls')}`);
              if (!accordionContent) {
                return;
              }
    
              accordionContent.classList.remove(options.accordionExpandedClass);
              accordionContent.setAttribute('aria-hidden', 'true');
              accordionContent.setAttribute('hidden', 'hidden');
              expandedContent.filter(content => content !== accordionContent);
              options.collapse(button, accordionContent);
              button.setAttribute('aria-expanded', 'false');
            }
          }
        };
    
        /**
         * Set all attributes and toggle visibility accordingly.
         */
        const setInitial = () => {
          for (let i = buttons.length; i--;) {
            setVisibility(buttons[i], true);
          }
        };
    
        const close = (button) => {
          button.setAttribute('aria-expanded', 'false');
          setVisibility(button);
        };
    
        /**
         * Closes all accordion items.
         */
        const closeAll = () => {
          for (let i = buttons.length; i--;) {
            close(buttons[i]);
          }
        };
    
        const open = (button) => {
          button.setAttribute('aria-expanded', 'true');
          setVisibility(button);
        };
    
        /**
         * Opens all accordion items.
         */
        const openAll = () => {
          for (let i = buttons.length; i--;) {
            open(buttons[i]);
          }
        };
    
        /**
         * Open the accordion-content related to the location hash.
         */
        const hashEvent = () => {
          let hash = window.location.hash.replace('#', '');
          if (!hash) {
            return;
          }
          try {
            const trigger = elem.querySelector(`[aria-controls=${hash}]`);
            if (trigger) {
              open(trigger);
              trigger.focus();
            }
          }
          catch (e) { }
        };
    
        /**
         * Enable accordion functionality.
         */
        const init = () => {
          setInitial();
          addEvents();
          hashEvent();
        };
    
        if (options.init !== false) {
          init();
        }
    
        return {init, closeAll, openAll};
      };
    }));
    
  • URL: /components/raw/accordion/accordion.functions.js
  • Filesystem Path: components/31-molecules/accordion/accordion.functions.js
  • Size: 7.5 KB