Carousel (based on a tabpanel)
Carousel (based on a tabpanel)
This carousel pattern is based on an ARIA tab panel pattern. Only one tab panel (carousel item) is visible at a time. There are buttons to go forward, back, or to play/pause the carousel.
Turn on a screen reader to experience this example in action.
- City Skyline
- Beach Sunset
- Roman Statue
Initial HTML Markup
<div class="deque-tabpanel" id="carousel">
<ul class="deque-tabpanel-tablist" role="tablist">
<li class="deque-tabpanel-tab" role="tab" id="tabCitySkyline" aria-controls="panelCitySkyline">
City Skyline
</li>
<li class="deque-tabpanel-tab" role="tab" id="tabSunset" aria-controls="panelSunset">
Beach Sunset
</li>
<li class="deque-tabpanel-tab" role="tab" id="tabRomanBuilding" aria-controls="panelRomanBuilding">
Roman Statue
</li>
</ul>
<div class="deque-tabpanel-tabpanels">
<div class="deque-tabpanel-tabpanel" role="tabpanel" id="panelCitySkyline" aria-labelledby="tabCitySkyline">
<img id="imgCitySkyline" src="assets/js/patterns/images/tempimage01.jpeg"
alt="City skyline">
</div>
<div class="deque-tabpanel-tabpanel" role="tabpanel" id="panelSunset" aria-labelledby="tabSunset">
<img id="imgSunset" src="assets/js/patterns/images/tempimage02.jpeg"
alt="Sunset over lake">
</div>
<div class="deque-tabpanel-tabpanel" role="tabpanel" id="panelRomanBuilding" aria-labelledby="tabRomanBuilding">
<img id="imgRomanBuilding" src="assets/js/patterns/images/tempimage03.jpeg"
alt="Old roman building with statue on top.">
</div>
</div>
<div class="deque-tabpanel-button-bar">
<button class="deque-button" id="prevButton">
prev
</button>
<button class="deque-button" id="playButton">
play
</button>
<button class="deque-button" id="pauseButton">
pause
</button>
<button class="deque-button" id="nextButton">
next
</button>
</div>
</div>
JavaScript
Required: The complete JavaScript file (for all patterns in the library): deque-patterns.min.js
Key parts of the JavaScript file
Note: The code below is functional only in the context of the complete JavaScript file.
In the @tabpanel section:
function createTabpanel(widget, config) {
var widgetTablist = widget.querySelector('[role=tablist]');
widgetTablist.classList.add('deque-tabpanel-tablist');
var pause = widget.querySelector('.deque-tabpanel-button-bar');
if (pause && !pause.classList.contains('hidden')) {
pause.click();
}
function initializeTabs(widget) {
var tabs = widget.querySelectorAll('[role=tab]');
for (var i = 0; i < tabs.length; i++) {
tabs[i].classList.add('deque-tabpanel-tab');
if (i == 0) {
tabs[i].setAttribute('aria-selected', 'true');
tabs[i].setAttribute('tabindex', '0');
} else {
tabs[i].setAttribute('aria-selected', 'false');
tabs[i].setAttribute('tabindex', '-1');
}
}
return tabs;
}
var tabs = initializeTabs(widget);
function initializePanels(widget) {
var panels = widget.querySelectorAll('[role=tabpanel]');
for (var i = 0; i < panels.length; i++) {
panels[i].classList.add('deque-tabpanel-tabpanel');
if (i != 0) {
panels[i].classList.add('deque-hidden');
}
}
return panels;
}
var panels = initializePanels(widget);
// create a live region to toss tab-panel-related announcements into
var region = (0, _containerUtils.createLiveRegion)();
document.body.appendChild(region);
var autoplayControls;
var autoplayConfig;
if (config.autoplay) {
autoplayConfig = {
onPlay: function onPlay() { },
onPause: function onPause() { },
onNext: function onNext() {
var nextTab = getNext(getCurrentTab(tabs));
selectTab(nextTab, tabs, panels);
region.notify(nextTab.innerText + 'tab');
},
onPrevious: function onPrevious() {
var previousTab = getPrev(getCurrentTab(tabs));
selectTab(previousTab, tabs, panels);
region.notify(previousTab.innerText + 'tab');
}
};
autoplayControls = (0, _carouselControls.activateCarouselControls)(widget, region, autoplayConfig, config.autoplay || 3000);
}
function selectTab(selectedTab, tabs, panels) {
var selectedTabLabeledBy = selectedTab.getAttribute('aria-controls');
for (var i = 0; i < panels.length; i++) {
if (panels[i].id === selectedTabLabeledBy) {
panels[i].classList.remove('deque-hidden');
} else {
panels[i].classList.add('deque-hidden');
}
}
for (var j = 0; j < tabs.length; j++) {
if (tabs[j].getAttribute('aria-controls') === selectedTabLabeledBy) {
tabs[j].setAttribute('tabindex', '0');
tabs[j].setAttribute('aria-selected', 'true');
} else {
tabs[j].setAttribute('tabindex', '-1');
tabs[j].setAttribute('aria-selected', 'false');
}
}
}
function applyNavigationLogic(tabs, panels, _ref) {
var autoselect = _ref.autoselect,
vertical = _ref.vertical;
var tabstopConfig = {
onSpace: function onSpace(item) {
var selectedTabLabeledBy = item.getAttribute('aria-controls');
for (var i = 0; i < panels.length; i++) {
if (panels[i].id === selectedTabLabeledBy) {
panels[i].classList.remove('deque-hidden');
} else {
panels[i].classList.add('deque-hidden');
}
}
},
onClick: function onClick(item) {
var selectedTabLabeledBy = item.getAttribute('aria-controls');
for (var i = 0; i < panels.length; i++) {
if (panels[i].id === selectedTabLabeledBy) {
panels[i].classList.remove('deque-hidden');
} else {
panels[i].classList.add('deque-hidden');
}
}
},
select: selectTab,
useAriaSelected: true,
autoselect: autoselect
};
if (vertical) {
tabstopConfig.onUp = function (item) {
if (autoplayControls) {
autoplayControls.pause();
}
return getPrev(item);
};
tabstopConfig.onDown = function (item) {
if (autoplayControls) {
autoplayControls.pause();
}
return getNext(item);
};
} else {
tabstopConfig.onLeft = function (item) {
if (autoplayControls) {
autoplayControls.pause();
}
return getPrev(item);
};
tabstopConfig.onRight = function (item) {
if (autoplayControls) {
autoplayControls.pause();
}
return getNext(item);
};
}
(0, _tabstopUtils.createSingleTabstopStructure)(tabs, panels, tabstopConfig);
}
applyNavigationLogic(tabs, panels, config);
function getCurrentTab(tabs) {
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].getAttribute('tabindex') === '0') {
return tabs[i];
}
}
}
function getPrev(item) {
var output = item.previousElementSibling || tabs[tabs.length - 1];
return output;
}
function getNext(item) {
var output = item.nextElementSibling || tabs[0];
return output;
}
}
function initializeAllTabpanels() {
var widgets = document.querySelectorAll('.deque-tabpanel');
var config;
for (var i = 0; i < widgets.length; i++) {
if (widgets[i].id == 'carousel') {
config = {
autoselect: true,
autoplay: 3000
};
} else {
config = {
autoselect: true,
autoplay: false
};
}
createTabpanel(widgets[i], config);
}
}
initializeAllTabpanels();
In the @tabstopUtils section:
function createSingleTabstopStructure(tabsNL, panels, config) {
var tabs = Array.prototype.slice.call(tabsNL);
tabs.forEach(function (tab) {
tab.addEventListener('focus', function () {
if (config.onFocus) {
config.onFocus(tab);
}
});
if (!config.select) {
config.select = function () { };
}
if (config.onClick) {
tab.addEventListener('click', function () {
config.onClick(tab);
config.select(tab, tabs, panels);
});
}
if (config.onSpace) {
kb.onElementSpace(tab, function (e) {
e.preventDefault();
e.stopPropagation();
config.onSpace(tab);
config.select(tab, tabs, panels);
});
}
if (config.onLeft) {
kb.onElementLeft(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onLeft(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
if (config.onRight) {
kb.onElementRight(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onRight(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
if (config.onUp) {
kb.onElementUp(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onUp(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
if (config.onDown) {
kb.onElementDown(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onDown(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
if (config.onHome) {
kb.onElementHome(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onHome(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
if (config.onEnd) {
kb.onElementEnd(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onEnd(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
if (config.onPageUp) {
kb.onElementPageUp(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onPageUp(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
if (config.onPageDown) {
kb.onElementPageDown(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onPageDown(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
if (config.onCharacter) {
kb.onElementCharacter(tab, function (e) {
e.preventDefault();
e.stopPropagation();
var target = config.onCharacter(tab);
if (target) {
target.focus();
if (config.autoselect) {
config.select(target, tabs, panels);
}
}
});
}
});
}
In the @tabpanel-carouselControls section:
function createButton(label) {
var classes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
var output = document.createElement('button');
output.innerText = label;
classes.forEach(function (c) {
return output.classList.add(c);
});
return output;
}
function buildCarouselControls(_ref) {
var onPlay = _ref.onPlay,
onPause = _ref.onPause,
onNext = _ref.onNext,
onPrevious = _ref.onPrevious;
var duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2000;
var output = document.createElement('div');
output.className = 'output-wrap';
var prevButton = createButton('prev', ['prev']);
var playButton = createButton('play', ['play', 'hidden']);
var pauseButton = createButton('pause', ['pause']);
var nextButton = createButton('next', ['next']);
// create a live region to toss tab-panel-related announcements into
var region = (0, _containerUtils.createLiveRegion)();
document.body.appendChild(region);
var playInterval = void 0,
called = false;
var startPlaying = function startPlaying() {
if (playInterval) {
stopPlaying();
}
playInterval = setInterval(next, duration);
playButton.classList.add('hidden');
pauseButton.classList.remove('hidden');
pauseButton.focus();
if (onPlay) {
onPlay();
}
region.notify('Carousel playing');
};
var stopPlaying = function stopPlaying() {
clearInterval(playInterval);
playInterval = null;
playButton.classList.remove('hidden');
if (called) {
playButton.focus();
}
pauseButton.classList.add('hidden');
if (onPause) {
onPause();
}
called = true;
region.notify('Carousel paused');
};
var next = function next() {
if (onNext) {
onNext();
}
};
var prev = function prev() {
if (onPrevious) {
onPrevious();
}
};
nextButton.addEventListener('click', next);
prevButton.addEventListener('click', prev);
playButton.addEventListener('click', startPlaying);
pauseButton.addEventListener('click', stopPlaying);
output.appendChild(prevButton);
output.appendChild(playButton);
output.appendChild(pauseButton);
output.appendChild(nextButton);
startPlaying();
output.start = startPlaying;
output.pause = stopPlaying;
output.prev = prev;
output.next = next;
return output;
}
function activateCarouselControls(widget, region, _ref2) {
var onPlay = _ref2.onPlay,
onPause = _ref2.onPause,
onNext = _ref2.onNext,
onPrevious = _ref2.onPrevious;
var duration = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 2000;
var output = widget.querySelector('.deque-tabpanel-button-bar');
var buttons = output.querySelectorAll('.deque-button');
var prevButton;
var playButton;
var pauseButton;
var nextButton;
for (var i = 0; i < buttons.length; i++) {
if (buttons[i].id == 'prevButton') {
prevButton = buttons[i];
} else if (buttons[i].id == 'playButton') {
playButton = buttons[i];
} else if (buttons[i].id == 'pauseButton') {
pauseButton = buttons[i];
} else if (buttons[i].id == 'nextButton') {
nextButton = buttons[i];
}
}
playButton.classList.add('deque-hidden');
playButton.classList.remove('deque-button');
var playInterval = false;
var startPlaying = function startPlaying() {
if (playInterval) {
stopPlaying();
}
playInterval = setInterval(next, duration);
playButton.classList.add('deque-hidden');
playButton.classList.remove('deque-button');
pauseButton.classList.remove('deque-hidden');
pauseButton.classList.add('deque-button');
pauseButton.focus();
if (onPlay) {
onPlay();
}
region.notify('Carousel playing');
};
var stopPlaying = function stopPlaying() {
clearInterval(playInterval);
playInterval = null;
playButton.classList.remove('deque-hidden');
playButton.classList.add('deque-button');
playButton.focus();
pauseButton.classList.add('deque-hidden');
pauseButton.classList.remove('deque-button');
if (onPause) {
onPause();
}
region.notify('Carousel paused');
};
var next = function next() {
if (onNext) {
onNext();
}
};
var prev = function prev() {
if (onPrevious) {
onPrevious();
}
};
nextButton.addEventListener('click', next);
prevButton.addEventListener('click', prev);
playButton.addEventListener('click', startPlaying);
pauseButton.addEventListener('click', stopPlaying);
startPlaying();
output.start = startPlaying;
output.pause = stopPlaying;
output.prev = prev;
output.next = next;
return output;
}
In the @guidUtils section:
/*
note - not a true guid. I prepend 'g' because
the ID of an element cannot start with a numeral
*/
function generateGuid() {
var S4 = function S4() {
return ((1 + Math.random()) * 0x10000 | 0).toString(16).substring(1);
};
return 'g' + (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4());
}
In the @containerUtils section:
function elementIsChildOfElement(child, potentialParent) {
while (child) {
if (child === potentialParent) {
return true;
}
child = child.parentNode;
}
return false;
}
function createFieldset(label) {
var fieldset = document.createElement('fieldset');
var legend = document.createElement('legend');
legend.classList.add('legend'); // for easy lookup regardless of mode
legend.id = (0, _guidUtils.generateGuid)();
legend.innerText = label;
fieldset.appendChild(legend);
return fieldset;
}
function createLiveRegion() {
var level = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'polite';
var classes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
var output = document.createElement('span');
classes.forEach(function (c) {
return output.classList.add(c);
});
output.id = (0, _guidUtils.generateGuid)();
output.setAttribute('aria-live', level);
output.classList.add('deque-visuallyhidden');
output.innerText = '';
output.notify = function (text) {
// TODO: Clean this up...no need to extend the element prototype
while (output.firstChild) {
output.removeChild(output.firstChild);
}
var msg = document.createElement('div');
msg.innerHTML = text;
output.appendChild(msg);
};
return output;
}
In the @keyboardUtils section:
var KEYS = exports.KEYS = {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
SHIFT: 16,
CTRL: 17,
ALT: 18,
ESCAPE: 27,
SPACE: 32,
LEFT: 37,
RIGHT: 39,
UP: 38,
DOWN: 40,
F10: 121,
HOME: 36,
END: 35,
PAGE_UP: 33,
PAGE_DOWN: 34
};
function bindElementToEventValue(element, eventName, testValue, handler) {
function localHandler(e) {
if (e.which === testValue) {
handler(e);
}
}
element.addEventListener(eventName, localHandler);
return function () {
element.removeEventListener(eventName, localHandler);
};
}
function bindElementToKeypressValue(element, testValue, handler) {
return bindElementToEventValue(element, 'keypress', testValue, handler);
}
function bindElementToKeydownValue(element, testValue, handler) {
return bindElementToEventValue(element, 'keydown', testValue, handler);
}
function onElementEnter(element, handler) {
return bindElementToKeydownValue(element, KEYS.ENTER, handler);
}
function onElementEscape(element, handler) {
return bindElementToKeydownValue(element, KEYS.ESCAPE, handler);
}
function onElementSpace(element, handler) {
return bindElementToKeypressValue(element, KEYS.SPACE, handler);
}
function onElementLeft(element, handler) {
return bindElementToKeydownValue(element, KEYS.LEFT, handler);
}
function onElementRight(element, handler) {
return bindElementToKeydownValue(element, KEYS.RIGHT, handler);
}
function onElementUp(element, handler) {
return bindElementToKeydownValue(element, KEYS.UP, handler);
}
function onElementDown(element, handler) {
return bindElementToKeydownValue(element, KEYS.DOWN, handler);
}
function onElementHome(element, handler) {
return bindElementToKeydownValue(element, KEYS.HOME, handler);
}
function onElementEnd(element, handler) {
return bindElementToKeydownValue(element, KEYS.END, handler);
}
function onElementPageUp(element, handler) {
return bindElementToKeydownValue(element, KEYS.PAGE_UP, handler);
}
function onElementPageDown(element, handler) {
return bindElementToKeydownValue(element, KEYS.PAGE_DOWN, handler);
}
function onElementF10(element, handler) {
return bindElementToKeydownValue(element, KEYS.F10, handler);
}
function isAlphaNumeric(charCode) {
return charCode >= 48 && charCode <= 90 /* numbers, uppercase letters */
|| charCode >= 97 && charCode <= 122 /* lowercase letters */;
}
function onElementCharacter(element, handler) {
function localHandler(e) {
var charCode = e.which;
if (isAlphaNumeric(charCode)) {
handler(e);
}
}
element.addEventListener('keypress', localHandler);
return function () {
element.removeEventListener('keypress', localHandler);
};
}
function trapEnter(element) {
onElementEnter(element, function (e) {
e.stopPropagation();
e.preventDefault();
});
}
Note: No additional JavaScript initialization code is necessary for this pattern. All elements with class="deque-tabpanel"
will be initialized automatically by the external JavaScript file.
CSS
Required: The complete CSS file (for all patterns in the library): deque-patterns.min.css
Key styles within the CSS file (other styles may also be necessary):
.deque-tabpanel [role='tablist'] {
list-style-type: none;
padding: 0;
display: flex;
margin: 0 0 20px 0;
justify-content: center;
}
.deque-tabpanel img {
max-width: 100%;
}
.deque-tabpanel ul.deque-tabpanel-tablist[role='tablist'] li {
font-size: 20px;
display: inline-block;
flex-shrink: 0;
font-weight: 200;
white-space: nowrap;
padding: 5px 12px;
cursor: pointer;
}
.deque-tabpanel ul.deque-tabpanel-tablist[role='tablist'] li:focus {
outline: 1px dashed #000000;
}
.deque-tabpanel li.deque-tabpanel-tab[role='tab'][aria-selected='true'] {
color: #000000;
border-bottom: 2px solid;
border-top: 2px solid;
}
.deque-tabpanel .deque-tabpanel-tabpanels {
clear: both;
padding: 0;
margin: 0;
}
.deque-tabpanel .deque-tabpanel-tabpanel[role='tabpanel'] {
width: 100%;
}
.deque-tabpanel .deque-tabpanel-button-bar {
text-align: center;
}
.deque-tabpanel .deque-tabpanel-button-bar .pause,
.deque-tabpanel .deque-tabpanel-button-bar .play {
margin: 0 15px;
}
@media screen and (max-width: 320px) {
.deque-tabpanel-tablist {
display: block !important;
}
.deque-tabpanel-tab {
width: 100% !important;
box-sizing: border-box;
}
}
Implementation Instructions
Implement the Tabpanel pattern when you want to provide multiple tabpanels of information (for example, either Landscape, Portrait, or Square) that are focusable via the Tab
key and navigable via the arrow
keys.
Step 1: Add Dependencies
Add deque-patterns.min.css in the <head>
of the document.
<link rel="stylesheet" type="text/css" href="deque-patterns.min.css">
Add a script link to deque-patterns.min.js to the bottom of the page.
<script type="text/javascript" src="deque-patterns.min.js"></script>
Step 2: Add HTML
- Create a
<div>
or<span>
container withclass="deque-tabpanel"
. If you would like the tabs to automatically cycle through addid="carousel"
. - Make a
<ul>
container withclass="deque-tabpanel-tablist"
androle="tablist"
. - List the tabs you want, each within a
<li>
container. Have them includeclass="deque-tabpanel-tab"
,role="tab"
, a unique id, and a unique aria-controls attribute. The inner text will be the title of the tab. - Create a
<div>
or<span>
container withclass="deque-tabpanel-tabpanels"
. - List the panels that correspond with the tabs with a
<div>
or<span>
container. These panels are the content that will be displayed when the tab is selected. Each needs to haveclass="deque-tabpanel-tabpanel"
androle="tabpanel"
. The panel's id is the same as aria-controls of the corresponding tab and there should be an aria-labelledby attribute that is the same as the corresponding tab's id. Within this<div>
is the content. That could include text or an<img>
container. - If your tabpanel is a carousel you can add buttons to control it. Initially, it's in the 'play' state, and the control contains three buttons: "next", "pause" and "previous". If you click pause, it enters the "pause" state and the buttons are "next", "play" and "previous".
- To create the buttons make a
<div>
or<span>
container withclass="deque-tabpanel-button-bar"
. - Each button needs a
<button>
container. Addclass="deque-button"
for styling purposes and an id ("prevButton", "playButton", "pauseButton", or "nextButton"). The inner text is the words that appear on the button itself.