The slider is progressively enhanced, with a basic scrollable div of items for no-js conditions.
Since it is enhanced, the dot nav is only needed when the JS runs.
The dot nav is thus generated in the JS based on the number of items in the slider.
It is very important to provide a meaningful short title/label for each slide in the data-slide-label
attribute. This label will be used as a tab title for the dot navigation and will be shown on hover.
The title of each dot is shown on hover and focus to make it possible for users of voice dictation to select and interact with the dot.
Labels should be short and succinct. They’re not meant to be long decsriptions like the image description, for example.
The slider requires a name to be announced to screen readers. The ARIA attributes are all already set. You only need to provide a short name in the data-aria-label
attribute. Note that this name does not need the word “slider” in it because “slider” will be announced in the aria-roledescription
attribute added in the JS.
The ARIA attributes are added in the JS not in the markup because, should the JS fail, the user will hear “slider” but won’t be able to interact with it as one. Only add the ARIA attributes to the interactive component when the component becomes interactive.
The paddle navigation (left/right arrow keys) are kept in the markup and hidden with the hidden
attribute by default, and only shown the JS runs.
Use the arrow icons you want. Make sure you add the SVG inline. Inline SVGs are the most accessible in environments such as Windows High Contrast Mode.
Try to abide to the markup in the HTML and use the data attributes as required in the HTML. If you change anything, make sure to adapt the CSS and JS to match your markup.
<section class="ps ps-slider ps-slider--testimonials">
<div class="ps__wrap">
<div class="ps__head">
<header class="ps__header">
<span class="ps__kicker">The Slider</span>
<h2 class="ps__title" aria-level="">A Collection of Testimonials</h2>
</header>
<div class="ps__desc">
<p>
our clients love us. Here is some of the love we got after working with them.
</p>
</div>
</div>
<div class="ps__main grid grid--12">
<div class="span-1-12">
<!-- the slider label should not have word "slider" in it -->
<div class="c-slider" data-slider data-aria-label="PROVIDE_LABEL">
<div class="c-slider__paddleNav" data-slider-paddlenav hidden>
<!-- paddleNav (prev and next buttons) provided here for flexibility (choose and use the icon you want) -->
<button class="c-slider__paddleNav__prev" aria-label="Previous Slide" data-prev>
<!-- place icon here -->
<svg width="8" height="12" viewBox="0 0 8 12" aria-hidden="true" focusable="false">
<g fill="none" fill-rule="evenodd">
<path d="M-8-6h24v24H-8z" />
<path fill="currentColor" fill-rule="nonzero" d="M7.41 10.59L2.83 6l4.58-4.59L6 0 0 6l6 6z" />
</g>
</svg>
</button>
<button class="c-slider__paddleNav__next" aria-label="Next Slide" data-next>
<!-- place icon here -->
<svg width="8" height="12" viewBox="0 0 8 12" aria-hidden="true" focusable="false">
<g fill="none" fill-rule="evenodd">
<path d="M-8-6h24v24H-8z" />
<path fill="currentColor" fill-rule="nonzero" d="M.59 10.59L5.17 6 .59 1.41 2 0l6 6-6 6z" />
</g>
</svg>
</button>
</div>
<!-- make sure to provide a readable label for each section to be used in the dotNav buttons -->
<div class="c-slider__slides-container" data-slides>
<div class="c-slider__slides-wrapper" data-slides-wrapper>
<div data-slide-label="this is slide one label" class="c-slider__slide" data-slide>
<blockquote class="ps-slider__testimonial">
<div class="ps-slider__testimonial__header">
<img class="ps-slider__testimonial__logo" src="../../img/testimonial-logo.svg" alt="LOGO_LABEL_HERE" />
</div>
<div class="ps-slider__testimonial__content">
<p>
“We have been able to make scaling great!”
</p>
</div>
<footer class="ps-slider__testimonial__footer">
<cite>
<span class="ps-slider__testimonial__author">John Doe #1</span>
<span class="ps-slider__testimonial__author-role">Co-Founder & CTO</span>
</cite>
</footer>
</blockquote>
</div>
<div data-slide-label="this is slide two label" class="c-slider__slide" data-slide>
<blockquote class="ps-slider__testimonial">
<div class="ps-slider__testimonial__header">
<img class="ps-slider__testimonial__logo" src="../../img/testimonial-logo.svg" alt="LOGO_LABEL_HERE" />
</div>
<div class="ps-slider__testimonial__content">
<p>
“We have been able to make scaling the technical infrastructure of our business cost effective and efficient.”
</p>
</div>
<footer class="ps-slider__testimonial__footer">
<cite>
<span class="ps-slider__testimonial__author">John Doe #2</span>
<span class="ps-slider__testimonial__author-role">Co-Founder & CTO</span>
</cite>
</footer>
</blockquote>
</div>
<div data-slide-label="this is slide three label" class="c-slider__slide" data-slide>
<blockquote class="ps-slider__testimonial">
<div class="ps-slider__testimonial__header">
<img class="ps-slider__testimonial__logo" src="../../img/testimonial-logo.svg" alt="LOGO_LABEL_HERE" />
</div>
<div class="ps-slider__testimonial__content">
<p>
“We have been able to make scaling the technical infrastructure of our business cost effective and efficient.”
</p>
</div>
<footer class="ps-slider__testimonial__footer">
<cite>
<span class="ps-slider__testimonial__author">John Doe #3</span>
<span class="ps-slider__testimonial__author-role">Co-Founder & CTO</span>
</cite>
</footer>
</blockquote>
</div>
<div data-slide-label="this is slide four label" class="c-slider__slide" data-slide>
<blockquote class="ps-slider__testimonial">
<div class="ps-slider__testimonial__header">
<img class="ps-slider__testimonial__logo" src="../../img/testimonial-logo.svg" alt="LOGO_LABEL_HERE" />
</div>
<div class="ps-slider__testimonial__content">
<p>
“We have been able to make scaling the technical infrastructure of our business cost effective and efficient.”
</p>
</div>
<footer class="ps-slider__testimonial__footer">
<cite>
<span class="ps-slider__testimonial__author">John Doe #4</span>
<span class="ps-slider__testimonial__author-role">Co-Founder & CTO</span>
</cite>
</footer>
</blockquote>
</div>
</div>
</div>
<!-- the dotNav is generated in the JS according to the number of slides / -->
<!---->
</div>
</div>
</div>
</div>
</div>
</section>
<script src="../../js/hammer.min.js"></script>
"use strict";
// prepend method polyfill for IE
(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty('prepend')) {
return;
}
Object.defineProperty(item, 'prepend', {
configurable: true,
enumerable: true,
writable: true,
value: function prepend() {
var argArr = Array.prototype.slice.call(arguments),
docFrag = document.createDocumentFragment();
argArr.forEach(function (argItem) {
var isNode = argItem instanceof Node;
docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
});
this.insertBefore(docFrag, this.firstChild);
}
});
});
})([Element.prototype, Document.prototype, DocumentFragment.prototype]);
if (typeof Object.assign != "function") {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, "assign", {
value: function assign(target, varArgs) {
// .length of function is 2
if (target == null) {
// TypeError if undefined or null
throw new TypeError(
"Cannot convert undefined or null to object"
);
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) {
// Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (
Object.prototype.hasOwnProperty.call(
nextSource,
nextKey
)
) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
// add utilities; some of them borrowed from: https://scottaohara.github.io/a11y_tab_widget/
var util = {
keyCodes: {
UP: 38,
DOWN: 40,
LEFT: 37,
RIGHT: 39,
HOME: 36,
END: 35,
ENTER: 13,
SPACE: 32,
DELETE: 46,
TAB: 9
},
generateID: function (base) {
return base + Math.floor(Math.random() * 999);
},
getDirectChildren: function (elm, selector) {
return Array.prototype.filter.call(elm.children, function (child) {
return child.matches(selector);
});
},
getUrlHash: function () {
return window.location.hash.replace('#', '');
},
/**
* Use history.replaceState so clicking through Tabs
* does not create dozens of new history entries.
* Browser back should navigate to the previous page
* regardless of how many Tabs were activated.
*
* @param {string} hash
*/
setUrlHash: function (hash) {
if (history.replaceState) {
history.replaceState(null, '', '#' + hash);
} else {
location.hash = hash;
}
},
/**
* Convert hyphen-separated string to camelCase
* Source: https://stackoverflow.com/questions/6660977/convert-hyphens-to-camel-case-camelcase
* CamelCase is readable by Screen Readers and so can be used as an accessible label for the dotNav
* More info: https://adrianroselli.com/2018/01/improving-your-tweet-accessibility.html#Hash
*/
dashToCamelCase: function (string) {
return string.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
},
dashToSpaces: function (string) {
return string.replace(/-/g, ' ');
}
};
(function (w, doc, undefined) {
var ARIAaccOptions = {
manual: false,
loop: false,
withDotNav: true
}
var ARIAslider = function (inst, options) {
var _options = Object.assign(ARIAaccOptions, options);
var el = inst;
var slidesContainer = el.querySelector("[data-slides]");
var slidesWrapper = el.querySelector("[data-slides-wrapper]");
var slides = Array.from(el.querySelectorAll("[data-slide]"));
var paddleNav = el.querySelector("[data-slider-paddleNav]");
var prevButton = paddleNav.querySelector('[data-prev]');
var nextButton = paddleNav.querySelector('[data-next]');
var sliderID = util.generateID('c-slider-');
var currentIndex = 0;
var selectedDot = 0;
var tempDot = 0;
var manual = _options.manual;
var loop = _options.loop;
var withDotNav = _options.withDotNav;
var dots = [];
var init = function () {
el.setAttribute('id', sliderID);
el.classList.add('js-slider');
el.setAttribute('role', 'group'); // or region
el.setAttribute('aria-roledescription', 'Slider');
el.setAttribute('aria-label', el.getAttribute('data-aria-label'));
// show Next and Prev Buttons
paddleNav.removeAttribute('hidden');
setupPaddleNav();
if (withDotNav) setupDotNav();
setupSlides();
setupSRHelper();
enableTouchSwipes();
};
var enableTouchSwipes = function () {
var mc = new Hammer(slidesWrapper, { threshold: 500 });
// listen to events...
mc.on("swipeleft", function (e) {
paddleForward();
});
mc.on("swiperight", function (e) {
paddleBack();
});
}
var setupSRHelper = function () {
let helper = document.createElement('span');
helper.setAttribute('aria-live', 'polite');
helper.setAttribute('id', sliderID + '__SRHelper');
helper.classList.add('sr-only');
helper.classList.add('c-slider__SRHelper');
el.prepend(helper);
updateHelper();
}
var updateHelper = function () {
let showing = slides[currentIndex].getAttribute('data-slide-label');
let showingNb = currentIndex + 1;
let helper = el.querySelector('.c-slider__SRHelper');
helper.innerHTML = 'Showing ' + showing + ', slide ' + showingNb + ' of ' + slides.length;
}
var setupPaddleNav = function () {
prevButton.addEventListener('keydown', (e) => {
paddleKeyboardRespond(e);
updateHelper();
}, false);
nextButton.addEventListener('keydown', (e) => {
paddleKeyboardRespond(e);
updateHelper();
}, false);
prevButton.addEventListener('click', function (e) {
paddleBack();
updateHelper();
});
nextButton.addEventListener('click', function (e) {
paddleForward(e);
updateHelper();
});
handlePaddleButtonsState();
}
var handlePaddleButtonsState = function () {
if (!loop && currentIndex == slides.length - 1) {
nextButton.setAttribute('aria-disabled', 'true');
nextButton.setAttribute('tabindex', '-1');
}
else if (!loop && currentIndex < slides.length - 1) {
nextButton.removeAttribute('aria-disabled');
nextButton.removeAttribute('tabindex');
}
if (!loop && currentIndex == 0) {
prevButton.setAttribute('aria-disabled', 'true');
prevButton.setAttribute('tabindex', '-1');
}
else if (!loop && currentIndex > 0) {
prevButton.removeAttribute('aria-disabled');
prevButton.removeAttribute('tabindex');
}
}
var setupDotNav = function () {
var dotNavList = document.createElement('div');
dotNavList.setAttribute('data-slider-dotNav', '');
dotNavList.setAttribute('role', 'tablist');
dotNavList.setAttribute('class', 'c-slider__dotNav');
// create dots
var nb = slides.length;
for (var i = 0; i < nb; i++) {
let dot = document.createElement('button');
dot.setAttribute('role', 'tab');
dot.setAttribute('id', sliderID + '__dot-' + i)
dot.setAttribute('class', 'c-slider__dotNav__dot')
dot.setAttribute('data-slider-dot', '') // to be used in the CSS as a hook if needed; not used elsewhere.
dot.setAttribute('data-controls', slides[i].getAttribute('id'));
let dotLabel = document.createElement('span');
dotLabel.textContent = slides[i].getAttribute('data-slide-label');
dotLabel.classList.add('dot-label');
// append dot to dotNavList
dotNavList.appendChild(dot);
dot.appendChild(dotLabel);
dots.push(dot);
}
dots.forEach((dot, index) => {
if (index === currentIndex) {
selectDot();
}
dot.addEventListener('click', (e) => {
e.preventDefault();
e.preventDefault();
selectedDot = index;
currentIndex = selectedDot;
focusCurrentDot();
selectDot();
handlePaddleButtonsState();
updateHelper();
}, false);
dot.addEventListener('keydown', (e) => {
dotKeyboardRespond(e, dot);
updateHelper();
}, false);
});
// append dotNavList to slider
el.appendChild(dotNavList);
}
var selectDot = function () {
// unactivate all other dots
dots.forEach(dot => {
dot.setAttribute('aria-selected', 'false');
dot.setAttribute('tabindex', '-1');
});
//activate current tab
dots[selectedDot].setAttribute('aria-selected', 'true');
dots[selectedDot].setAttribute('tabindex', '0');
// activate corresponding panel
activateCurrentSlide();
}
var setupSlides = function () {
slides.forEach((slide, index) => {
if (_options.withDotNav) {
slide.setAttribute('role', 'tabpanel');
slide.setAttribute('aria-labelledby', dots[index].getAttribute('id'));
}
else {
slide.setAttribute('role', 'group');
slide.setAttribute('aria-roledescription', 'Slide');
}
slide.setAttribute('tabindex', '-1');
slide.setAttribute('data-hidden', 'true');
slide.addEventListener('keydown', (e) => {
// slideKeyboardRespond(e);
}, false);
slide.addEventListener("blur", () => {
slide.setAttribute('tabindex', '-1');
}, false);
});
activateCurrentSlide();
}
var slideKeyboardRespond = function (e) {
var keyCode = e.keyCode || e.which;
switch (keyCode) {
case util.keyCodes.TAB:
slides[currentIndex].setAttribute('tabindex', '-1');
break;
default:
break;
}
}
var activateCurrentSlide = function () {
slides.forEach((slide, index) => {
slide.setAttribute('data-hidden', 'true');
slide.removeAttribute('tabindex');
});
slides[currentIndex].setAttribute('data-hidden', 'false');
// if (withDotNav) slides[currentIndex].setAttribute('aria-labelledby', dots[currentIndex].getAttribute('id'));
slides[currentIndex].setAttribute('tabindex', '0');
slideToCurrentSlide(currentIndex);
}
var slideToCurrentSlide = function () {
var translateValue = currentIndex * -100;
slidesWrapper.style.transform = 'translateX(' + translateValue + '%)';
}
var incrementcurrentIndex = function () {
if (currentIndex < slides.length - 1) {
return ++currentIndex;
}
else {
if (loop) {
currentIndex = 0;
return currentIndex;
}
else return;
}
};
var decrementcurrentIndex = function () {
if (currentIndex > 0) {
return --currentIndex;
}
else {
if (loop) {
currentIndex = slides.length - 1;
return currentIndex;
}
else return;
}
};
var paddleBack = function (e) {
decrementcurrentIndex();
activateCurrentSlide();
handlePaddleButtonsState();
selectedDot = currentIndex;
tempDot = selectedDot;
if (withDotNav) selectDot();
}
var paddleForward = function (e) {
incrementcurrentIndex();
activateCurrentSlide();
handlePaddleButtonsState();
selectedDot = currentIndex;
tempDot = selectedDot;
if (withDotNav) selectDot();
}
var incrementTempDot = function () {
if (tempDot < dots.length - 1) {
return ++tempDot;
}
else {
if (loop) {
tempDot = 0;
return tempDot;
}
else return;
}
};
var decrementTempDot = function () {
if (tempDot > 0) {
return --tempDot;
}
else {
if (loop) {
tempDot = slides.length - 1;
return tempDot;
}
else {
return;
}
}
};
var focusCurrentDot = function () {
dots[tempDot].focus();
}
var moveBack = function (e) {
e.preventDefault();
decrementTempDot();
focusCurrentDot();
if (!manual) {
selectedDot = tempDot;
currentIndex = selectedDot;
selectDot();
activateCurrentSlide();
handlePaddleButtonsState();
}
}
var moveForward = function (e) {
e.preventDefault();
incrementTempDot();
focusCurrentDot();
if (!manual) {
selectedDot = tempDot;
currentIndex = selectedDot;
selectDot();
activateCurrentSlide();
handlePaddleButtonsState();
}
}
var dotKeyboardRespond = function (e, dot) {
var firstDot = dots[0];
var lastDot = dots[dots.length - 1];
var keyCode = e.keyCode || e.which;
switch (keyCode) {
case util.keyCodes.UP:
case util.keyCodes.LEFT:
moveBack(e);
break;
case util.keyCodes.DOWN:
case util.keyCodes.RIGHT:
moveForward(e);
break;
case util.keyCodes.ENTER:
case util.keyCodes.SPACE:
e.preventDefault();
selectedDot = tempDot;
currentIndex = selectedDot;
handlePaddleButtonsState();
selectDot();
break;
case util.keyCodes.TAB:
slides[selectedDot].setAttribute('tabindex', '0');
selectedDot = tempDot;
currentIndex = selectedDot;
break;
case util.keyCodes.HOME:
e.preventDefault();
firstTab.focus();
break;
case util.keyCodes.END:
e.preventDefault();
lastTab.focus();
break;
}
}
var paddleKeyboardRespond = function (e) {
var keyCode = e.keyCode || e.which;
switch (keyCode) {
case util.keyCodes.LEFT:
prevButton.focus();
paddleBack(e);
break;
case util.keyCodes.RIGHT:
nextButton.focus();
paddleForward(e);
break;
case util.keyCodes.ENTER:
case util.keyCodes.SPACE:
selectedDot = currentIndex;
selectDot();
break;
case util.keyCodes.TAB:
slides[selectedDot].setAttribute('tabindex', '0');
currentIndex = selectedDot;
break;
}
}
init.call(this);
return this;
}; // ARIAslider()
w.ARIAslider = ARIAslider;
})(window, document);
var sliderInstance = "[data-slider]";
var els = document.querySelectorAll(sliderInstance);
var allslider = [];
// Generate all slider instances
for (var i = 0; i < els.length; i++) {
var nslider = new ARIAslider(els[i]); // if manual is set to false, the slider open on focus without needing an ENTER or SPACE press
allslider.push(nslider);
}
{
"status": "ready"
}
/**************************************************\
* Slider | Testimonials component-specific styles.
* Main general styles are in the _slider.scss file
***************************************************/
.ps-slider--testimonials {
.ps-slider__testimonial {
width: 100%;
margin: 0;
margin-bottom: var(--c-margin);
padding: 4rem 1.5rem;
background-color: var(--color--secondary);
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
@media all and (min-width: 50em) {
padding: calc(var(--v-space) / 1.25) var(--c-padding);
}
}
.ps-slider__testimonial__logo {
margin-bottom: var(--v-margin);
}
.ps-slider__testimonial__content {
line-height: 1.3;
line-height: 1.2;
font-size: calc(1rem * var(--text-min-xl));
max-width: 50ch;
margin: 0 auto;
@media screen and (min-width: 40rem) {
font-size: calc(calc(1rem * var(--text-min-xl)) + (var(--text-max-xl) - var(--text-min-xl)) * (100vw - 40rem) / (80 - 40));
}
@media screen and (min-width: 80rem) {
font-size: calc(1rem * var(--text-max-xl));
}
p:last-of-type() {
margin-bottom: 0;
}
}
.ps-slider__testimonial__footer {
margin-top: var(--v-margin);
}
.ps-slider__testimonial__footer cite {
font-style: normal;
}
.ps-slider__testimonial__author {
font-weight: bold;
font-size: calc(1rem * var(--text-min-l));
@media screen and (min-width: 40rem) {
font-size: calc(calc(1rem * var(--text-min-l)) + (var(--text-max-l) - var(--text-min-l)) * (100vw - 40rem) / (80 - 40));
}
@media screen and (min-width: 80rem) {
font-size: calc(1rem * var(--text-max-l));
}
}
.ps-slider__testimonial__author,
.ps-slider__testimonial__author-position {
display: block;
margin-bottom: .5rem;
}
}
title: Slider | Testimonials
# notes: "You can have a note here; it will override the README.md notes"
context:
text: