A New Way to Create Accessible Accordions. Guide to the Jolty.js
Tags: javascript, webdev, frontend, codepen
Published: 2023-10-14 17:43:05

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.

```html
<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>
```

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

```js
import { Tablist } from "jolty";
Tablist.initAll();
```

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

{% codepen https://codepen.io/joltylabs/pen/Yzdbwvg %}

## Transition

For creating transitions, it uses [CSS-Based Transitions](https://vuejs.org/guide/built-ins/transition.html#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](https://jolty.dev) generates special CSS variables `--ui-transition-width` and `--ui-transition-height`. This allows us to decide ourselves how to animate our content.

```html
<div data-ui-tablist-tabpanel class="accordion-panel" hidden>...</div>
```

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

```css
@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;
   }
}
```

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

```html
<div data-ui-tablist-tabpanel class="accordion-panel" hidden>
  <div class="accordion-panel-inner">...</div>
</div>
```

```css
.accordion-panel-inner {
  padding: 1.25rem;
}
```

## Nested Accordion

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

```html
<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>
```

## 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.

```html
<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>
```

## Custom structure

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

```html
<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>
```

```js
new Tablist('#faq', {
   tab: ".faq-tab",
   tabpanel: ".faq-tabpanel",
});
```

## 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`.

```html
<div class="faq-list" data-ui-tablist="faq">...</div>
```

```js
Tablist.data('faq', {
   tab: ".faq-tab",
   tabpanel: ".faq-tabpanel",
});

Tablist.initAll();
```

## 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.

```html
<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>
```

We can also make use of the new [@media scripting](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/scripting) now.
[Browser support](https://caniuse.com/mdn-css_at-rules_media_scripting).

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.

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

Tablist.initAll();
```

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

```css
.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;
   }
}
```

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.

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

Tablist.initAll();
```

## 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.

```html
<div data-ui-tablist data-ui-multi-expanded>...</div>
```

## 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.

```html
<div data-ui-tablist data-ui-always-expanded>...</div>
```

## Using hidden=until-found attribute

The following example works only in browsers on the Blink engine.
[Browser support](https://caniuse.com/mdn-html_global_attributes_hidden_until-found_value)

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`.

```html
<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>
```

## 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](https://jolty.dev).