DEV Community

Cover image for A New Way to Create Accessible Accordions. Guide to the Jolty.js
Anatolii Moldovanov 🇺🇦
Anatolii Moldovanov 🇺🇦

Posted on • Edited on

A New Way to Create Accessible Accordions. Guide to the Jolty.js

An accordion is a very popular UI component used to organise and hide content to avoid overwhelming the user, but is it that easy to create one? Let's find out.

According to the ARIA design pattern for accordions, you should provide keyboard control, set roles, assign unique IDs, and link them through aria-attributes, sounds complicated, doesn't it?

What if I tell you that you don't have to do anything, all you need to do is create a structure using data-attributes.

Getting Started

By implementing data-attributes, let's go through a basic way to create an accordion that doesn't require additional settings.

<div data-ui-tablist>
  <div data-ui-tablist-item>
    <button data-ui-tablist-tab>Accordion button #1 <span>↓</span></button>
    <div data-ui-tablist-tabpanel hidden>Accordion content #1</div>
  </div>
  <div data-ui-tablist-item>
    <button data-ui-tablist-tab>Accordion button #2 <span>↓</span></button>
    <div data-ui-tablist-tabpanel hidden>Accordion content #2</div>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

After that, install the Jolty and call the static method initAll() on the Tablist class.

import { Tablist } from "jolty";
Tablist.initAll();
Enter fullscreen mode Exit fullscreen mode

Now, when clicking on the <button>, our content will open and close.

Transition

For creating transitions, it uses CSS-Based Transitions, as in Vue.js., This means special CSS classes, responsible for a specific transition phase, are added during opening and closing.

Also, Jolty generates special CSS variables --ui-transition-width and --ui-transition-height. This allows us to decide ourselves how to animate our content.

<div data-ui-tablist-tabpanel class="accordion-panel" hidden>...</div>
Enter fullscreen mode Exit fullscreen mode

We can use @media (prefers-reduced-motion: no-preference) to apply transitions only if the user has not enabled the setting on their device for reduced motion

@media (prefers-reduced-motion: no-preference) {
   .accordion-panel.ui-enter-active,
   .accordion-panel.ui-leave-active {
     transition-duration: 200ms;
     transition-property: height, opacity;
     transition-timing-function: ease-in-out;
     overflow: hidden;
   }
   .accordion-panel.ui-enter-to,
   .accordion-panel.ui-leave-from {
     height: var(--ui-transition-height);
   }
   .accordion-panel.ui-enter-from,
   .accordion-panel.ui-leave-to {
     height: 0;
     opacity: 0;
   }
}
Enter fullscreen mode Exit fullscreen mode

If we want to use the padding CSS property, this element must be placed inside the tabpanel.

<div data-ui-tablist-tabpanel class="accordion-panel" hidden>
  <div class="accordion-panel-inner">...</div>
</div>
Enter fullscreen mode Exit fullscreen mode
.accordion-panel-inner {
  padding: 1.25rem;
}
Enter fullscreen mode Exit fullscreen mode

Nested Accordion

To create nested accordions, you don't need to set additional configurations, just ensure that each level contains its own tablist wrapper.

<div data-ui-tablist>
  <div data-ui-tablist-item>
    <button data-ui-tablist-tab>Accordion button level 1 <span>↓</span></button>
    <div data-ui-tablist-tabpanel hidden>
      Accordion content level 1
      <div data-ui-tablist>
        <div data-ui-tablist-item>
          <button data-ui-tablist-tab>Accordion button level 2<span>↓</span></button>
          <div data-ui-tablist-tabpanel hidden>Accordion content level 2</div>
        </div>
        ...
      </div>
    </div>
  </div>
  ...
</div>
Enter fullscreen mode Exit fullscreen mode

Hash navigation

If you want the tabpanel to open when the URL contains a hash with its id, you can assign it a unique id, and for tab set the data-ui-tablist-tab="{id}" attribute.

<div data-ui-tablist>
  <div data-ui-tablist-item>
    <button data-ui-tablist-tab="my-tabpanel">Accordion button #1 <span>↓</span></button>
    <div id="my-tabpanel" hidden>Accordion content #1</div>
  </div>
  ...
</div>
Enter fullscreen mode Exit fullscreen mode

Custom structure

If you do not want to use data-attributes, you can define your own selectors by modifying the tab and tabpanel options.

<div class="faq-list" id="faq">
    <button class="faq-tab">How do I track my order?</button>
    <div class="faq-tabpanel" hidden>...</div>
    <button class="faq-tab">How do I reset my password</button>
    <div class="faq-tabpanel" hidden>...</div>
    <button class="faq-tab">Can I change my order after placing it?</button>
    <div class="faq-tabpanel" hidden>...</div>
 </div>
Enter fullscreen mode Exit fullscreen mode
new Tablist('#faq', {
   tab: ".faq-tab",
   tabpanel: ".faq-tabpanel",
});
Enter fullscreen mode Exit fullscreen mode

Settings Groups

But what if we do not want to specify unique ids or want to use the same settings for other accordions? There is a solution for this - use the static method Tablist.data().

Let's rewrite our example above to get rid of the id.

<div class="faq-list" data-ui-tablist="faq">...</div>
Enter fullscreen mode Exit fullscreen mode
Tablist.data('faq', {
   tab: ".faq-tab",
   tabpanel: ".faq-tabpanel",
});

Tablist.initAll();
Enter fullscreen mode Exit fullscreen mode

SEO-friendly structure and @media scripting: none

As you may have noticed, we used a <button> for the tab in the example above, which is unsuitable for search bots. Let's change the <button> to an <h3> tag.

<div class="faq-list" id="faq">
    <h3 class="faq-tab">How do I track my order?</h3>
    <div class="faq-tabpanel">...</div>
    <h3 class="faq-tab">How do I reset my password</h3>
    <div class="faq-tabpanel">...</div>
    <h3 class="faq-tab">Can I change my order after placing it?</h3>
    <div class="faq-tabpanel">...</div>
 </div>
Enter fullscreen mode Exit fullscreen mode

We can also make use of the new @media scripting now.
Browser support.

As you may have noticed above, we removed the hidden attribute. Instead, we will use the option hideMode: 'class-shown'. In this case, Jolty will check whether the tabpanel contains the .ui-shown class. If not, it will be considered closed.

Tablist.data('faq', {
   tab: ".faq-tab",
   tabpanel: ".faq-tabpanel",
   hideMode: "class-shown",
});

Tablist.initAll();
Enter fullscreen mode Exit fullscreen mode

Let's define styles for the .ui-shown class.

.faq-tabpanel:where(:not(.ui-shown)) {
   display: none;
   /* If JavaScript is not enabled in the browser, don't hide it */
   @media (scripting: none) {
      display: initial;
   }
}
Enter fullscreen mode Exit fullscreen mode

Since @media (scripting: none) still doesn't have broad support, we utilise progressive enhancement, applying styles only when it is supported, therefore we use display: initial, but you can use any other value.

Breakpoints

Jolty allows us to set settings for a specific screen size through the breakpoints option, or destroy the element at the needed breakpoint. Let's do this for screens larger than 640px.

To do this, we will create a new settings group 'faq-mobile' and inherit settings from the 'faq' group.

Tablist.data('faq-mobile', {
   data: 'faq'
   breakpoints: {
      640: {
         destroy: true,
      },
   },
});

Tablist.initAll();
Enter fullscreen mode Exit fullscreen mode

Multi-expanded Option

By default, when a user opens a new tab, the previous one will automatically close. Enabling the multiExpanded option will keep the previous tabs open.

<div data-ui-tablist data-ui-multi-expanded>...</div>
Enter fullscreen mode Exit fullscreen mode

Always-expanded Option

By default, a user can close all tabs, but if you want always to have at least one tab remain open, you should set the alwaysExpanded: true option.

<div data-ui-tablist data-ui-always-expanded>...</div>
Enter fullscreen mode Exit fullscreen mode

Using hidden=until-found attribute

The following example works only in browsers on the Blink engine.
Browser support

If the content is hidden, it will not be accessible using a find-in-page search. To remedy this, we can use the hidden=until-found attribute for tabpanel.

<div class="faq-list" data-ui-tablist>
    <button data-ui-tablist-tab>How do I track my order?</button>
    <div data-ui-tablist-tabpanel hidden="until-found">...</div>
    <button data-ui-tablist-tab>How do I reset my password</button>
    <div data-ui-tablist-tabpanel hidden="until-found">...</div>
    <button data-ui-tablist-tab class="faq-tab">Can I change my order after placing it?</button>
    <div data-ui-tablist-tabpanel hidden="until-found">...</div>
 </div>
Enter fullscreen mode Exit fullscreen mode

To Be Continued

This article explored an example of creating an accordion using the Tablist class. Next, we will discuss creating tabs using the same component.

Follow us to ensure you don't miss the second part; meanwhile, you can read the documentation on our website.

Top comments (0)