Accessibility

Overview

While carousels can attractively represent website content in less space, they have fatal accessibility problems. For example, even the basic action — changing slides and reading a newly displayed content — can quickly become hard when you do that through only a screen reader.

Splide strives to improve accessibility so that all users can easily and comfortably use the carousel. Splide v4 conforms to the W3C Carousel Design Pattern and includes a Live Region that allows screen readers to read content whenever slides change.

But unfortunately, the W3C design pattern is not perfect because:

  • It has self-contradiction
  • It does not cover all kinds of carousels

For instance, their examples are limited to "fade" carousels, and therefore simply applying them to a "sliding" type can break our carousel. Because of this failure, other carousels adopting the pattern have defective accessibility.

Considering these points, this page describes how Splide improve its accessibility.

The W3C design pattern has three sub-patterns, Basic, Tabbed and Grouped. All Splide carousels will become either Basic or Tabbed, determined by existence of pagination:

PatternDescription
BasicApplied to carousels without pagination
TabbedApplied to carousels with pagination. The pagination is implemented as Tabs Pattern
GroupedN/A

The Tabbed pattern makes carousels behave like a Tabs UI, treating slides as tabpanel and pagination dots as tab. Tabs UI requires complex keyboard interaction and tabindex management explained below.

I desire role="carousel", since a carousel is not relevant to a Tabs UI. They may be technically similar, but a carousel is definitely not a Tabs UI.

Landmark

A carousel container element that encompasses all components of the carousel, including both carousel controls and slides, has either role region or role group. — Basic carousel elements

In general, carousels contain information that are directly related with the main page contents — e.g. banners, gallery, recommended articles, product list, etc. By default, Splide changes carousels into region landmarks, which tells assistive technologies that they are part of main contents so that users benefit from them, such as quickly navigating to landmarks.

But in case that the carousel is not related with the main content or is just decoration, the landmark role is not appropriate. For such cases, set the group role by HTML:

<div class="splide" role="group" aria-label="...">
</div>
HTML

...or by options:

new Splide( '.splide', { role: 'group' } );
JavaScript

Keyboard Interaction

From the version 4, custom keyboard shortcuts, such as arrow keys to move a carousel, are disabled by default, but only the shortcuts defined in Tabs Pattern are enabled.

Key Bindings

When pagination contains focus, Splide handles these keys to control the carousel:

KeyDescription
In horizontal pagination, moves focus to the previous (or next in RTL) dot and displays the corresponding slide
In horizontal pagination, moves focus to the next (or previous in RTL) dot when pagination contains focus and displays the corresponding slide
In vertical pagination, moves focus to the previous dot and displays the corresponding slide
In vertical pagination, moves focus to the next dot and displays the corresponding slide
SpaceDisplays the slide if it's not active
EnterDisplays the slide if it's not active
HomeMoves the focus to the first dot and displays the first slide
EndMoves the focus to the last dot and displays the last slide

Basically, moving focus to an indicator dot activates the corresponding slide (called Having selection follow focus). But if waitForTransition is enabled, the selection may not follow the focus while transitioning.

You can enable or disable keyboard shortcuts by keyboard and paginationKeyboard options.

Pagination Direction

By default, the pagination direction is determined by the direction option. For example, if you set direction to 'ttb', Splide treats pagination as "vertical" regardless of how your pagination is visually displayed, assigning aria-orientation: vertical to the element and observing / to move focus.

In case that you'd like to use horizontal pagination in the vertical carousel, or vise versa, you can override the direction by the paginationDirection option:

new Splide( '.splide', {
direction : 'ttb',
paginationDirection: 'ltr',
} );
JavaScript

Roving Tabindex

Splide manages focus within pagination by roving tabindex, which is required for tabbed interface. It sets tabindex="-1" to all buttons in pagination except for the active dot so that they are removed from the page Tab sequence.

Benefits of using this technique are that:

  • Keyboard users can easily move focus back to the previously selected dot after they leave the carousel

  • Keyboard users can leave the pagination just hitting a Tab once instead of moving focus through all buttons

Try it in the example below. Click the next button first, and hit the Tab key. Also, you can check how arrow keys, Home, and End work as explained above.

  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09

Live Region

When a hidden slide gets visible, it's not easy to read the newly displayed content through a screen reader. We can visually recognize it, but users who rely on auditory or Braille information have to find the updated slide.

The solution is nothing but Live Region that can convey dynamic contents to assistive technologies — however, it does NOT work for some screen readers by just adding the aria-live="polite" attribute because common carousels do not satisfy the aria-relevant condition for them to detect slide change.

After a long struggle to solve this problem, I finally managed to get it to work! 🎉

Cool, right?😃

But this is definitely annoying if autoplay is enabled because screen readers keep speaking while the page is active even if the carousel is out of the viewport. Splide disables a live region when autoplay starts, and enables it again when autoplay gets paused.

A carousel with isNavigation: ture will never activate a live region since it controls another carousel. Also, you can disable it by the live option.

I've tested the live region by Windows Narrator, JAWS, NVDA, and Voice Over — but I cannot guarantee it works for all screen readers (impossible to test all of them on all browsers and devices😱).

Autoplay Play/Pause (WCAG 2.2.1, 2.2.2)

The user is allowed to turn off the time limit before encountering it — Timing Adjustable

For any auto-updating information that starts automatically and is presented in parallel with other content, there is a mechanism for the user to pause, stop, or hide it. — Pause, Stop, Hide

W3C suggests that the carousel with autoplay must satisfy following things:

  • Have a button for pausing/resuming autoplay
  • Should pause autoplay when focus comes into the carousel
  • Should pause autoplay while the mouse is hovering over the carousel

Users with disabilities — such as blindness, dexterity impairments, and cognitive limitations — need adequate time to read contents. If your carousel can automatically rotate, you should also give users a button to pause/resume autoplay. Splide provides the simple way to install a play-pause toggle button built with the Toggle Button design pattern.

About 2 remaining requirements, you don't have to do anything because Splide enables both pauseOnHover and pauseOnFocus options by default.

Reducing Motion (WCAG 2.3.3)

Motion animation triggered by interaction can be disabled, unless the animation is essential to the functionality or the information being conveyed. — Animation from Interactions

Some people feel headaches and nausea — such as those with vestibular motion disorders — by animations and transitions. When Splide detects prefers-reduced-motion: reduce:

  • Overrides speed and rewindSpeed to 0
  • Overrides autoplay to paused so that it will not start after page load

Consequently, the carousel immediately displays the active slide without transition effects. The exception is the transition made by drag and swipe since it can be considered as "essential" motion for users to understand what is happening on the carousel.

You can emulate the media feature by Chrome DevTools. Open the "Rendering" tab and find "Emulate CSS media feature prefers-reduced-motion".

  • 01
  • 02
  • 03
  • 04
  • 05
  • 06
  • 07
  • 08
  • 09

Do not rely on only autoplay for changing slides, unless the carousel is just for a decorative purpose. Otherwise, the user will have no way to rotate the carousel while prefers-reduced-motion is enabled.

Focus Visible (WCAG 2.4.7)

Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible. — Focus Visible

The criterion is saying we have to add styles when focusable elements gets focus for keyboard users. To display the focus indicator only for them, we need to use a :focus-visible pseudo-class supported by most major browsers, but:

  • IE does not support it (IE will be EOL though)
  • About Safari, only the latest version supports it (released 2022/03/15)

So, I've decided to temporarily patch :focus-visible until the class is available for most users (I really didn't want to, but no choice😫). Themes in v4 include focus styles for arrows, pagination and thumbnails.

Click the previous arrow once and try hitting Tab:

  • Sample Slide 01
  • Sample Slide 02
  • Sample Slide 03
  • Sample Slide 04
  • Sample Slide 05
  • Sample Slide 06
  • Sample Slide 07
  • Sample Slide 08
  • Sample Slide 09
  • Sample Thumbnail 01
  • Sample Thumbnail 02
  • Sample Thumbnail 03
  • Sample Thumbnail 04
  • Sample Thumbnail 05
  • Sample Thumbnail 06
  • Sample Thumbnail 07
  • Sample Thumbnail 08
  • Sample Thumbnail 09

If you are styling a carousel without themes, use the is-focus-in class that will be assigned to the root element when a carousel receives focus by keyboard.

/* For browsers that support :focus-visible */
.splide__arrow:focus-visible {
outline: 3px solid skyblue;
}
/* For other browsers */
.splide.is-focus-in .splide_arrow:focus {
outline: 3px solid skyblue;
}
CSS

Note that this class will not be removed immediately after the carousel loses focus. Make sure to use it with a :focus pseudo class.

Focusable Elements (WCAG 1.3.1)

Information, structure, and relationships conveyed through presentation can be programmatically determined or are available in text. — Info and Relationships

Splide hides slides which are not completely visible in the viewport by using the aria-hidden attribute on them from screen readers and other assistive technologies. If they contain focusable elements — such as <input> and <textarea> — users are still able to reach them by Tab, but screen readers won't explain what they are.

To solve this problem, Splide assigns tabindex="-1" to following focusable elements in hidden slides:

  • <a>
  • <button>
  • <textarea>
  • <input>
  • <select>
  • <iframe>

This approach works well in most cases, but fails if a slide contains these other elements:

  • <area>
  • <audio controls>
  • <summary>
  • <video controls>
  • An element with the contenteditable attribute
  • An element with tabindex="0"

By changing the focusableNodes option (this is a query string), you can handle them other than the tabindex attribute:

{
focusableNodes: 'a, button, ..., area, [contenteditable]',
}
JavaScript

Unfortunately, the option above can not handle tabindex="0", since the index should be restored when the ascendant slide becomes visible. I believe no one controls the tabindex attribute in a carousel, but you can manually toggle the index by listening to visible and hidden events.

Role and ARIA Attributes (WCAG 4.1.2)

For all user interface components, the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies. — Name, Role, Value

Splide assigns appropriate attributes to elements that consist of a carousel. All you have to do is add an accessible name and translate texts for your users.

Root

role

region if the element is not <section>.

aria-roledescription

carousel

Track

aria-live

polite if a live region is enabled. It turns into off when autoplay starts, and polite again when it's paused

aria-atomic

true if a live region is enabled (^4.0.3)

aria-busy

If a live region is enabled, true when transition ends, and false after some delay (^4.0.3).

Slides

role

group

aria-roledescription

slide

aria-label

%s of %s as "{slide number} of {slide length}"

aria-hidden

true for slides out of the view port.

When isNavigation option is enabled:

role

button if slideFocus is enabled, or otherwise group

aria-roledescription

Not assigned because the slide is interactive.

aria-hidden

true for slides out of the view port.

aria-controls

The ID or IDs which the carousel sync with.

aria-label

"Go to slide %s"

Arrows

aria-controls

The ID of the track element.

aria-label

"Previous slide" or "Go to last slide" for the previous arrow. "Next slide" or "Go to first slide" for a next arrow.

disabled

Assigned when there is no slide to go

Play/Pause Toggle Button

aria-controls

The ID of the track element.

aria-label

"Start autoplay" while autoplay is paused and "Pause autoplay" while autoplay is playing.

A tabbed carousel has same attributes with a basic one, except for following elements:

Slides

role

tabpanel

aria-roledescription

slide

aria-label

%s of %s as "{slide number} of {slide length}"

aria-hidden

true for slides out of the view port.

Pagination

role

tablist

aria-label

"Select slide to show".

Pagination List Item

role

presentation

Pagination Buttons

role

tab

aria-controls

The ID or IDs of slides.

aria-label

"Go to slide %s" or "Go to page %s".

aria-selected

true for the active dot and false for other dots.

Concerns

I still have a few concerns. If you are an expert of accessibility, please give me your feedback.

When the tab list contains the focus, moves focus to the next element in the page tab sequence outside the tablist, which is the tabpanel unless the first element containing meaningful content inside the tabpanel is focusable.

According to the W3C design pattern, each tabpanel in Tab UI should be focusable with tabindex="0" (in most cases). In their example, you'll see panels are focusable.

But MDN says:

Focusable elements should have interactive semantics.

They contradict each other since tabpanel is not an interactive semantic. Actually, Firefox DevTools complains that their example does not meet WCAG standards. I don't care if it's really violation or not, but I'd like to know:

  • Which is "practically" accessible?
  • Should slides in a carousel with the Tabbed Pattern be focusable or not? If it should be, slides in a carousel with the Basic Pattern should be also focusable for consistency. Because both patterns have same aria-roledescription="carousel", users must expect same usability for all carousels.