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.
Basic and Tabbed Carousel
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:
Pattern | Description |
---|---|
Basic | Applied to carousels without pagination |
Tabbed | Applied to carousels with pagination. The pagination is implemented as Tabs Pattern |
Grouped | N/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
<div class="splide" role="group" aria-label="..."> </div>
...or by options:
new
Splide
(
'.splide'
,
{
role
:
'group'
}
)
;
JavaScript
new Splide( '.splide', { role: 'group' } );
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:
Key | Description |
---|---|
← | 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 |
Space | Displays the slide if it's not active |
Enter | Displays the slide if it's not active |
Home | Moves the focus to the first dot and displays the first slide |
End | Moves 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
new Splide( '.splide', { direction : 'ttb', paginationDirection: 'ltr', } );
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
andrewindSpeed
to0
- Overrides
autoplay
topaused
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:
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
:
3
px
solid
skyblue
;
}
/* For other browsers */
.splide.is-focus-in
.splide_arrow:focus
{
outline
:
3
px
solid
skyblue
;
}
CSS
/* 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; }
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
{ focusableNodes: 'a, button, ..., area, [contenteditable]', }
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.
Basic Carousel
Root
role |
|
---|---|
aria-roledescription |
|
Track
aria-live |
|
---|---|
aria-atomic |
|
aria-busy | If a live region is enabled, |
Slides
role |
|
---|---|
aria-roledescription |
|
aria-label |
|
aria-hidden |
|
When isNavigation
option is enabled:
role |
|
---|---|
aria-roledescription | Not assigned because the slide is interactive. |
aria-hidden |
|
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. |
Tabbed Carousel
A tabbed carousel has same attributes with a basic one, except for following elements:
Slides
role |
|
---|---|
aria-roledescription |
|
aria-label |
|
aria-hidden |
|
Pagination
role |
|
---|---|
aria-label | "Select slide to show". |
Pagination List Item
role |
|
---|
Pagination Buttons
role |
|
---|---|
aria-controls | The ID or IDs of slides. |
aria-label | "Go to slide %s" or "Go to page %s". |
aria-selected |
|
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.