This is a follow-up of my Blog about JS / CSS Tabs. It introduces a way how tabs could be displayed in an Accordion manner without changing the structure of HTML. The source code for tabs is re-used, HTML structure is the same, some JS is overridden, some CSS is added.
I called that UI-control Tabcordion. It was inspired by Transformer Tabs on the CSS-Tricks web site. The difference is that Tabcordion (1) is not responsive, more it tries to replace Accordion, and (2) pretends to be ergonomic - the tab-title does not "jump".
Here is an example:
Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim.
At atqui choro theophrastus sit, ne erroribus vulputate vis, eam et antiopam scripserit. Et sea altera salutandi iudicabit. Vidisse probatus moderatius cum te, et vis feugiat luptatum consulatu.
Nec an stet decore honestatis, omittam maiestatis ei quo, eripuit facilis recusabo ius cu. Eu oblique detraxit honestatis vim, hinc sonet definitiones has ea.
At atqui choro theophrastus sit, ne erroribus vulputate vis, eam et antiopam scripserit. Et sea altera salutandi iudicabit. Vidisse probatus moderatius cum te, et vis feugiat luptatum consulatu.
Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim.
Nec an stet decore honestatis, omittam maiestatis ei quo, eripuit facilis recusabo ius cu. Eu oblique detraxit honestatis vim, hinc sonet definitiones has ea.
The menu-button on the right is visible when all other tab-bars are hidden. When you click the bar, all other tab-bars will show, pushing the currently showing content down. Clicking the bar once more will hide them again, pulling up the content. But when you click one of the other tab-bars, its content will replace the current content. Any tab-bar below the clicked one will disappear then, but none of those above (→ difference to Transformer Tabs).
The disadvantage of Tabcordion is that not all tabs are initially visible. There is a constructor parameter that can change this, but then it looks like tabs that have been arranged vertically.
This might not be a UI-control you want to use, but it shows how well-written JS source could be reused to show completely different results, doing just a few overrides, and adding some CSS.
Click on tab-bar above to navigate to the source code base I'm going to extend now. It is packed in script
and style
tags, uncommented, ready for copy & paste onto your web page. Mind that style
go to head
, while script
tags should be at end of the body
.
For a description of that source go to my last Blog about Tabs.
1 | <script type="text/javascript"> |
[aria-hidden = 'true'] {
display: none;
}
.tabbed-pane > [role = 'tablist'] > [role = 'tab'] {
display: inline-block;
border-radius: 0 1em 0 0;
margin-right: 0.16em;
}
[role = 'tablist'] {
list-style: none;
margin: 0;
padding: 0;
}
[role = 'tab'][aria-selected = 'true'] {
color: white;
background-color: gray;
cursor: initial;
}
[role = 'tab'] {
padding: 0.45em 0.4em 0.2em 0.3em;
color: black;
background-color: #F0F0F0;
cursor: pointer;
margin: 0;
}
[role = 'tab']:focus {
outline: 0;
border: 1px solid red;
}
[role = "tabpanel"] {
padding: 0.2em;
background-color: Khaki;
}
Here is the module outline I am going to implement now, in a script
tag. It extends the given instantiated tabBase
module (parameter) by setting it to the local that
object, and then replacing functions on it. (The tabBase
was called tabModule
in my last Blog.)
<script type="text/javascript">
/**
* Create a tabcordion.
* @param initiallyExpanded when true, tabcordions will be initially expanded,
* else they will be collapsed and the menu icon will show on selected button.
* @returns the object to call init() upon to find tabbed panes and tabcordions.
*/
var tabcordion = function(tabBase, keyboard, initiallyExpanded)
{
initiallyExpanded = (initiallyExpanded === undefined) ? false : initiallyExpanded;
var that = tabBase;
var initializing = true;
....
var superInit = that.init;
/**
* Builds tabbed panes found in given topElement (or current document).
* Overrides must be done before calling this.
* @param topElement optional, default is document.body,
* the top-element where to search for tabbed-panes.
*/
that.init = function(topElement) {
superInit(topElement);
initializing = false;
};
return that;
};
</script>
The initiallyExpanded
parameter defaults to false
to make tab-buttons below the focused one initially invisible. Passing true
would change that Tabcordion behavior.
The local initializing
flag will be used to mark the initialization phase. The init()
override sets it to false
at end of its execution.
Take this source code to the module outline where the "...." is.
var superSetButtonFocus = that.setButtonFocus;
/**
* Called when a tab was activated by the user.
* This implementation calls super, and then, when isFocused is true,
* toggles CSS "aria-expanded" on the parent container (role="tablist").
* @param tabButton the clicked tab-button.
* @param isFocused true when the tab is going to foreground, false when going to background.
*/
that.setButtonFocus = function(tabButton, isFocused) {
var wasFocused = (tabButton.getAttribute("aria-selected") === "true");
superSetButtonFocus(tabButton, isFocused);
if (isFocused) { // only the focused button can collapse or expand the list parent
var tablist = that.getExpandableParent(tabButton);
if (initializing) // set initial expansion state to the list
tablist.setAttribute("aria-expanded", initiallyExpanded ? "true" : "false");
else if ( ! wasFocused ) // when changing selection, again set initial expansion state to list
tablist.setAttribute("aria-expanded", initiallyExpanded ? "true" : "false");
else // when keeping selection state, toggle expansion state of list
toggleExpandable(tablist);
}
};
var toggleExpandable = function(tablist) {
var expanded = (tablist.getAttribute("aria-expanded") === "true");
tablist.setAttribute("aria-expanded", expanded ? "false" : "true");
};
/**
* This implementation assumes that the "aria-expanded" attribute is carried by direct parent.
* @return the parent element of given button that carries the "aria-expanded" attribute.
*/
that.getExpandableParent = function(tabButton) {
return tabButton.parentElement;
};
var superInstallListeners = that.installListeners;
that.installListeners = function(tabButton, tabPanelsWithButtons, selectedIndex, allTabPanelsWithButtons) {
superInstallListeners(tabButton, tabPanelsWithButtons, selectedIndex, allTabPanelsWithButtons);
if ( ! tabButton.getAttribute("title") )
tabButton.setAttribute("title", "Click to show or hide other tabs");
tabButton.addEventListener(that.observedKeyEvent, function(event) {
var key = keyboard.getKey(event);
if (key === "Enter") {
event.stopPropagation();
event.preventDefault();
toggleExpandable(that.getExpandableParent(tabButton));
}
});
};
Mind that no CSS is set by JS, just WAI-ARIA attributes are used to change the UI state. CSS rules that refer to these attributes will do the real work.
Extending the base module is done by saving function-pointers to local variables and then overwriting these functions. I always call the saved pointers superXXX
, for a function XXX
. This functional-inheritance-technique is needed when you want to call the super-functions from the override.
Just two functions were overridden:
setButtonFocus()
is called on initialization, and whenever the user clicks tab-button. It calls super
to do the normal tabbing work, and then, when the tab was focused, toggles the visibility state of the tab-buttons below the current one by setting "aria-expanded"
.installListeners()
was overwritten to also catch the ENTER key on tab-buttons, which should toggle the visibility state of the tab-buttons below. Besides it also puts a tooltip on the tab-button.The getExpandableParent()
function refers to the list of tab-buttons. It was made public to be overridden for other determination ways than the direct parent element.
This must go to a style
tag in head
of your page.
/* tabcordion tab button, with rounded corners and showing always a hand-cursor */
.tabcordion > [role = 'tablist'] > [role = 'tab'] {
border-radius: 0.5em 0.5em 0 0;
cursor: pointer;
margin-bottom: 0.1em;
}
/* show all tabcordion buttons when list is expanded */
.tabcordion > [role = 'tablist'][aria-expanded = 'true'] > [role = 'tab'] {
display: block;
}
/*
Hide buttons below selected when list is collapsed.
This variant does not jump, but items above are always visible.
*/
.tabcordion > [role = 'tablist'][aria-expanded = 'false'] > [role = 'tab'][aria-selected = 'true'] ~ [role = 'tab'] {
display: none;
}
/* tabcordion tablist is parent for absolutely positioned menu-icon */
.tabcordion > [role = 'tablist'] {
position: relative;
}
/* menu-icon as pseudo-element at the very right, visible only when list collapsed */
.tabcordion > [role = 'tablist'][aria-expanded = 'false'] > [role = 'tab'][aria-selected = 'true']::after {
content: '\02630';
margin-right: 0.8em;
position: absolute;
right: 0;
font-family: monospace;
font-size: 120%;
}
/* left indent of a nested tabcordion */
.tabcordion > [role = "tabpanel"] > .tabcordion {
padding-left: 1.3em;
}
Please read the inline comments of these CSS rules. Some of them are just decorative.
Essential is the toggling of tab-button list, done by the two rules
- .tabcordion > [role = 'tablist'][aria-expanded = 'true'] > [role = 'tab']
- .tabcordion > [role = 'tablist'][aria-expanded = 'false'] > [role = 'tab'][aria-selected = 'true'] ~ [role = 'tab']
The selector [role = "tab"]
denotes tab-buttons.
The CSS">
" operator denotes a direct child, and "~
" any following sibling on same level.
CSS rules always point to the rightmost described element.
Thus the second rule says that tab-buttons below the focused one should not be visible when the list of tab-buttons is not aria-expanded
.
The .tabcordion > [role = "tabpanel"] > .tabcordion
rule indents nested tabcordions. This makes it look a little bit like a tree.
Here is an example Tabcordion.
Following is a Tabcordion containing
You can see it in action on top of this page.
The only structural difference between tabbed pane and Tabcordion is the CSS class on the top-level div
element. Thus you could easily turn any Tabcordion into a tabbed pane just by changing the CSS class from "tabcordion" to "tabbed-pane".
<div class="tabcordion">
<ul role="tablist" aria-expanded='true'><li
id="tab-1" role="tab" aria-controls="tab-panel-1" aria-selected="true">Tab One</li><li
id="tab-2" role="tab" aria-controls="tab-panel-2" aria-selected="false">Tab Two</li><li
id="tab-3" role="tab" aria-controls="tab-panel-3" aria-selected="false">Tab Three</li><li>
</ul>
<div role="tabpanel" id="tab-panel-1" aria-labelledby="tab-1">
<p>Lorem ipsum dolor sit amet, qui meliore deserunt at.</p>
</div>
<div role="tabpanel" id="tab-panel-2" aria-labelledby="tab-2">
<div class="tabcordion">
<ul role="tablist"><li
id="tab-2-1" role="tab" aria-controls="tab-panel-2-1" aria-selected="true">Tab Two One</li><li
id="tab-2-2" role="tab" aria-controls="tab-panel-2-2" aria-selected="false">Tab Two Two</li><li
id="tab-2-3" role="tab" aria-controls="tab-panel-2-3" aria-selected="false">Tab Two Three</li>
</ul>
<div role="tabpanel" id="tab-panel-2-1" aria-labelledby="tab-2-1">
<p>At atqui choro theophrastus sit, ne erroribus vulputate vis, eam et antiopam scripserit. </p>
</div>
<div role="tabpanel" id="tab-panel-2-2" aria-labelledby="tab-2-2">
<p>Nec an stet decore honestatis, omittam maiestatis ei quo, eripuit facilis recusabo ius cu.</p>
</div>
<div role="tabpanel" id="tab-panel-2-3" aria-labelledby="tab-2-3">
<p>At atqui choro theophrastus sit, ne erroribus vulputate vis, eam et antiopam scripserit. </p>
</div>
</div>
</div>
<div role="tabpanel" id="tab-panel-3" aria-labelledby="tab-3">
<div class="tabbed-pane">
<ul role="tablist"><li
id="tab-3-1" role="tab" aria-controls="tab-panel-3-1" aria-selected="true">Tab Three One</li><li
id="tab-3-2" role="tab" aria-controls="tab-panel-3-2" aria-selected="false">Tab Three Two</li><li>
</ul>
<div role="tabpanel" id="tab-panel-3-1" aria-labelledby="tab-3-1">
<p>Lorem ipsum dolor sit amet, qui meliore deserunt at.</p>
</div>
<div role="tabpanel" id="tab-panel-3-2" aria-labelledby="tab-3-2">
<p>Nec an stet decore honestatis, omittam maiestatis ei quo, eripuit facilis recusabo ius cu.</p>
</div>
</div>
</div>
</div>
</div>
Following script should be at end of the body
of your page.
<script type="text/javascript">
var domFinder= domUtil();
var keyboardUtil = keyboard();
var tabs = tabBase(domFinder, keyboardUtil);
var initiallyExpanded = false;
tabcordion(tabs, keyboardUtil, initiallyExpanded).init();
</script>
All involved JS modules are instantiated and built together here. Finally init()
is called to make the tabcordion
module instantiation do its work.
Tabs with WAI-ARIA are a little painful. You always have to provide ids and id-references for aria-controls
and aria-labelled-by
. Moreover the HTML structure, as proposed by WAI-ARIA, makes it hard to switch between Tabbed-Pane and Accordion without restructuring the HTML, because of the <ul role="tablist">
on top, containing all tab-buttons. For an Accordion, the tab-button would have to be directly above its tab-panel.
In a future Blog I will try to describe how we can organize Tabbed-Panes and Accordions transparently, without having to restructure the HTML. Why don't we want to restructure HTML, for example using JavaScript? Because CSS hardcodes the HTML structure sometimes, and CSS is often written without knowledge about the JS working in the page.
ɔ⃝ Fritz Ritzberger, 2016-04-03