Accessibility
Overview
Splide follows W3C Carousel Concepts to improve accessibility as much as possible. Basically, they recommend that the accessible slider should guarantee these 4 requirements:
- People using keyboard navigation and voice input software can navigate between individual items.
- People using screen readers will understand which item is currently shown and how to navigate between items.
- People who are distracted by movement can pause animations.
- People who need more time to read can pause animations, providing them with sufficient time to read and understand carousel content.
The Autoplay component already deals with the last two problems. W3C suggests providing play/pause button, but it's up to you.
This page explains how Splide tackles the first two problems.
Keyboard
Keyboard Shortcuts
The user can control the slider by keyboard shortcuts binding these actions:
Shortcut | Description |
---|---|
← | Go to the previous page in LTR or the next page in RTL |
→ | Go to the next page in LTR or the previous page in RTL |
↑ | Go to the previous page in TTB |
↓ | Go to the previous page in TTB |
By default, Splide listens to the keydown
event of the document
,
which means all slides simultaneously correspond with shortcuts if you have multiple slides in the page.
If you are not comfortable about this behaviour, switch the keyboard
option to 'focused'
,
and the keyboard can navigate the slider only when it contains the active (focused) element.
This option inserts tabindex="0"
to the root element since the slider may have no focusable element.
With the Intersection extension, you can enable keyboard shortcuts only when the slider is in the viewport.
Tab Control
The user can navigate through visible slides, arrows and pagination by the Tab key:
- 01
- 02
- 03
- 04
- 05
- 06
- 07
- 08
- 09
By setting the slideFocus
option to false
, you can disable each slide from getting focused,
but this does not satisfy the first requirement mentioned earlier.
You need to emphasize the focused element by yourself.
Pagination
This W3C guide also recommends setting focus to the active slide for keyboard and assistive technology users:
When users select an item with those navigation buttons, the focus should be set on the selected item. In this case, the focus needs to be set to the
<li>
element that has the class current set, after the change or transition. This makes interaction easier for keyboard and assistive technology users.
Although Splide v2 does not have such functionality, I've implemented it for v3. Try clicking the pagination item, and you'll see the focus moves to the active slide:
- 01
- 02
- 03
- 04
- 05
- 06
- 07
- 08
- 09
Focusable Elements
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.
But if they contain focusable elements, such as <input>
and <textarea>
,
keyboard users are still able to reach them by the Tab key.
Lighthouse detects such elements and complains:
Focusable descendents within an
[aria-hidden="true"]
element prevent those interactive elements from being available to users of assistive technologies like screen readers.
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 slider,
but you can manually toggle the index by listening
to visible
and hidden
events.
ARIA Attributes
Every control has appropriate ARIA attributes for assistive technologies. All you have to do is translate texts for your users.
Slides
aria-hidden |
|
---|
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. |
Also, arrows will have the disabled
attribute when there is no slide to go.
Pagination
aria-controls | The ID or IDs of slides. |
---|---|
aria-label | "Go to slide %s" or "Go to page %s". |
Play/Pause
aria-controls | The ID of the track element. |
---|---|
aria-label | "Start autoplay" for the play button. "Pause autoplay" for the pause button. |
Navigation
If the isNavigation
option is true
, the slider behaves as the navigation of another slider.
Splide assigns to the menu
role to the list element and the menuitem
to each slide.
List Element
role |
|
---|
Each Slide
role |
|
---|---|
aria-controls | The ID or IDs which the slider sync with. |
aria-label | "Go to slide %s" |
Announcing The Active Slide
All that Splide lacks is announcing the active slide by the WAI-ARIA region. In this page, W3C says:
Use a WAI-ARIA live region to inform screen reader users what item is currently shown.
But also, I know there is dispute that announcing the active slide every time when the slider moves can be annoying, especially when autoplay is active. I've decided not to include this functionality in this version, but I'll show you how to implement it as an extension instead:
export
function
LiveRegion
(
Splide
)
{
let
liveRegion
;
function
mount
(
)
{
liveRegion
=
document
.
createElement
(
'div'
)
;
liveRegion
.
setAttribute
(
'aria-live'
,
'polite'
)
;
liveRegion
.
setAttribute
(
'aria-atomic'
,
'true'
)
;
liveRegion
.
classList
.
add
(
'visually-hidden'
)
;
Splide
.
root
.
appendChild
(
liveRegion
)
;
Splide
.
on
(
'moved'
,
onMoved
)
;
}
function
onMoved
(
)
{
liveRegion
.
textContent
=
`
Slide
${
Splide
.
index
+
1
}
of
${
Splide
.
length
}
`
;
}
function
destroy
(
)
{
Splide
.
root
.
removeChild
(
liveRegion
)
;
}
return
{
mount
,
destroy
,
}
}
JavaScript
export function LiveRegion( Splide ) { let liveRegion; function mount() { liveRegion = document.createElement( 'div' ); liveRegion.setAttribute( 'aria-live', 'polite' ); liveRegion.setAttribute( 'aria-atomic', 'true' ); liveRegion.classList.add( 'visually-hidden' ); Splide.root.appendChild( liveRegion ); Splide.on( 'moved', onMoved ); } function onMoved() { liveRegion.textContent = `Slide ${ Splide.index + 1 } of ${ Splide.length }`; } function destroy() { Splide.root.removeChild( liveRegion ); } return { mount, destroy, } }
This extension does 2 things:
- Creates a live region field
- Updates the text after the slider moves
And then, we need to "visually" hide the text by the well known technique:
.visually-hidden
{
border
:
0
;
clip
:
rect
(
0
0
0
0
)
;
height
:
1
px
;
margin
:
-1
px
;
overflow
:
hidden
;
padding
:
0
;
position
:
absolute
;
width
:
1
px
;
}
CSS
.visually-hidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; }
Finally, initialize Splide with registering the extension:
import
{
LiveRegion
}
from
'...'
;
new
Splide
(
'.splide'
)
.
mount
(
{
LiveRegion
}
)
;
JavaScript
import { LiveRegion } from '...'; new Splide( '.splide' ).mount( { LiveRegion } );
You can test the result in the following example. Start your screen reader and try moving the slider. It will inform you of the current slider number every time when the transition ends:
- 01
- 02
- 03
- 04
- 05
- 06
- 07
- 08
- 09