<div id="example--default--3" class="cwf-tabs">
<nav class="cwf-tabs__nav" role="tablist" aria-label="Tabs">
<div class="cwf-tabs__tabs">
<button id="cwf-tabs__tab--example--default--1" class="cwf-tabs__tab" aria-selected="true" aria-controls="cwf-tabs__panel--example--default--1" role="tab" tabindex="0">
Default tab 1
</button>
<button id="cwf-tabs__tab--example--default--2" class="cwf-tabs__tab" aria-selected="false" aria-controls="cwf-tabs__panel--example--default--2" role="tab" tabindex="-1">
Default tab 2
</button>
<button id="cwf-tabs__tab--example--default--3" class="cwf-tabs__tab" aria-selected="false" aria-controls="cwf-tabs__panel--example--default--3" role="tab" tabindex="-1">
Default tab 3
</button>
</div>
</nav>
<section id="cwf-tabs__panel--example--default--1" class="cwf-tabs__panel" aria-labelledby="cwf-tabs__tab--example--default--1" aria-hidden="false" role="tabpanel" tabindex="0">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi
condimentum egestas mi a rhoncus. Praesent at vehicula est. Curabitur
faucibus, mi ut commodo fringilla, urna magna hendrerit ex, eget
vehicula justo elit vel nisi. Aenean quam eros, dictum ullamcorper augue
eu, sodales accumsan risus. Praesent mollis ipsum eget nisl posuere
rutrum. Quisque lacus nunc, porttitor et ex at, posuere molestie turpis.
Integer sodales auctor quam, nec placerat elit eleifend et. Pellentesque
lacinia pulvinar leo et tincidunt. Aenean volutpat ipsum leo, a ornare
lectus eleifend nec. In at risus sit amet lorem consectetur laoreet quis
non libero. Quisque vulputate tellus ut vestibulum dignissim.
</p>
</section>
<section id="cwf-tabs__panel--example--default--2" class="cwf-tabs__panel" aria-labelledby="cwf-tabs__tab--example--default--2" aria-hidden="true" role="tabpanel" tabindex="-1">
<p>
Fusce rhoncus, nisl sit amet porta pharetra, ex nibh aliquet purus, ac
pellentesque justo ante vel ante. Duis et rhoncus risus. Etiam sagittis
risus et dui volutpat tristique. Orci varius natoque penatibus et magnis
dis parturient montes, nascetur ridiculus mus. Aenean rhoncus ac massa
non faucibus. Cras dui sapien, vehicula et felis id, dapibus auctor
neque. <a href="https://www.vcu.edu/">
Virginia Commonwealth University
</a> Vestibulum posuere sem pharetra erat sodales, ut rutrum orci dignissim.
Phasellus id lacus faucibus urna porttitor ultricies at id magna. Maecenas
malesuada nisi ut sollicitudin scelerisque. Vivamus hendrerit nulla a sapien
facilisis efficitur. Nulla commodo euismod urna.
</p>
</section>
<section id="cwf-tabs__panel--example--default--3" class="cwf-tabs__panel" aria-labelledby="cwf-tabs__tab--example--default--3" aria-hidden="true" role="tabpanel" tabindex="-1">
<p>
Integer mollis consectetur dolor, a gravida magna egestas sed. Donec
iaculis in velit nec lobortis. Maecenas cursus ornare fermentum. Duis ut
nulla ut libero consequat bibendum a et justo. Aenean id ligula tempus,
interdum magna et, pulvinar nunc. Praesent condimentum tristique
maximus. Mauris rhoncus mi in odio dapibus eleifend. Aenean bibendum
ullamcorper egestas. Cras nec imperdiet velit. Nulla pellentesque,
mauris quis vehicula vestibulum, mauris purus vehicula risus, in
accumsan mauris sem eget diam. Mauris sed venenatis nisl. Suspendisse
sit amet velit volutpat, sagittis quam id, sodales erat. Ut iaculis
pharetra leo. Praesent tincidunt sit amet purus ut tristique. Aliquam
sed metus diam.
</p>
</section>
</div>
{# Tabs nav and panels #}
{%- set tabs_nav = [] -%}
{%- set tabs_panels = [] -%}
{%- for panel in panels -%}
{% set id = panel.id ?? loop.index %}
{% set tab_id = 'cwf-tabs__tab--' ~ id %}
{% set panel_id = 'cwf-tabs__panel--' ~ id %}
{% set tabindex = loop.first ? 0 : '-1' %}
{% set tabs_nav =
tabs_nav|merge(
[
{
id: tab_id,
selected: loop.first,
panel: panel_id,
tabindex: tabindex,
text: panel.title
}
]
)
%}
{% set tabs_panels =
tabs_panels|merge(
[
{
id: panel_id,
tab: tab_id,
hidden: not loop.first,
tabindex: tabindex,
content: panel.content
}
]
)
%}
{%- endfor -%}
{%- set classes = ['cwf-tabs'] -%}
{%- if vertical and vertical in [true, 'left', 'right'] -%}
{% if vertical == true %}
{% set vertical = 'left' %}
{% endif %}
{% set classes = classes|merge(['cwf-tabs--' ~ vertical]) %}
{%- endif -%}
{%- if alignment and alignment in [true, 'start', 'center', 'end'] -%}
{% if alignment == true %}
{% set alignment = 'start' %}
{% endif %}
{% set classes = classes|merge(['cwf-tabs--' ~ alignment]) %}
{%- endif -%}
<div id="{{ id ?? 'cwf-tabs' }}" class="{{ classes|join(' ') }}">
<nav class="cwf-tabs__nav"
role="tablist"
aria-label="{{ label ?? 'Tabs' }}">
<div class="cwf-tabs__tabs">
{% for button in tabs_nav %}
<button id="{{ button.id }}"
class="cwf-tabs__tab"
aria-selected="{{ button.selected }}"
aria-controls="{{ button.panel }}"
role="tab"
tabindex="{{ button.tabindex }}">
{{ button.text }}
</button>
{% endfor %}
</div>
</nav>
{% for panel in tabs_panels %}
<section id="{{ panel.id }}"
class="cwf-tabs__panel"
aria-labelledby="{{ panel.tab }}"
aria-hidden="{{ panel.hidden }}"
role="tabpanel"
tabindex="{{ panel.tabindex }}">
{{ panel.content }}
</section>
{% endfor %}
</div>
{
"id": "example-tabs--default",
"panels": [
{
"id": "example--default--1",
"title": "Default tab 1",
"content": "<p>\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi\n condimentum egestas mi a rhoncus. Praesent at vehicula est. Curabitur\n faucibus, mi ut commodo fringilla, urna magna hendrerit ex, eget\n vehicula justo elit vel nisi. Aenean quam eros, dictum ullamcorper augue\n eu, sodales accumsan risus. Praesent mollis ipsum eget nisl posuere\n rutrum. Quisque lacus nunc, porttitor et ex at, posuere molestie turpis.\n Integer sodales auctor quam, nec placerat elit eleifend et. Pellentesque\n lacinia pulvinar leo et tincidunt. Aenean volutpat ipsum leo, a ornare\n lectus eleifend nec. In at risus sit amet lorem consectetur laoreet quis\n non libero. Quisque vulputate tellus ut vestibulum dignissim.\n</p>"
},
{
"id": "example--default--2",
"title": "Default tab 2",
"content": "<p>\n Fusce rhoncus, nisl sit amet porta pharetra, ex nibh aliquet purus, ac\n pellentesque justo ante vel ante. Duis et rhoncus risus. Etiam sagittis\n risus et dui volutpat tristique. Orci varius natoque penatibus et magnis\n dis parturient montes, nascetur ridiculus mus. Aenean rhoncus ac massa\n non faucibus. Cras dui sapien, vehicula et felis id, dapibus auctor\n neque. <a href=\"https://www.vcu.edu/\">\n Virginia Commonwealth University\n </a> Vestibulum posuere sem pharetra erat sodales, ut rutrum orci dignissim.\n Phasellus id lacus faucibus urna porttitor ultricies at id magna. Maecenas\n malesuada nisi ut sollicitudin scelerisque. Vivamus hendrerit nulla a sapien\n facilisis efficitur. Nulla commodo euismod urna.\n</p>"
},
{
"id": "example--default--3",
"title": "Default tab 3",
"content": "<p>\n Integer mollis consectetur dolor, a gravida magna egestas sed. Donec\n iaculis in velit nec lobortis. Maecenas cursus ornare fermentum. Duis ut\n nulla ut libero consequat bibendum a et justo. Aenean id ligula tempus,\n interdum magna et, pulvinar nunc. Praesent condimentum tristique\n maximus. Mauris rhoncus mi in odio dapibus eleifend. Aenean bibendum\n ullamcorper egestas. Cras nec imperdiet velit. Nulla pellentesque,\n mauris quis vehicula vestibulum, mauris purus vehicula risus, in\n accumsan mauris sem eget diam. Mauris sed venenatis nisl. Suspendisse\n sit amet velit volutpat, sagittis quam id, sodales erat. Ut iaculis\n pharetra leo. Praesent tincidunt sit amet purus ut tristique. Aliquam\n sed metus diam.\n</p>"
}
]
}
// Tabs component styles
// ---- Modules ----
@import "../../shared/style";
@import "../../shared/theme";
// ---- Wrapper ----
$tabs--display: flex !default;
$tabs--flex-direction: column !default;
$tabs--align-items: flex-start !default;
$tabs--border-color: lighten(style__color(white-darkest), 6.5%) !default;
$tabs--active-color: theme__accent--foreground() !default;
$tabs--inactive-background-color: style__color(white-dark) !default;
$tabs--inactive-color: style__color(gray-light) !default;
$tabs--accent-color: theme__accent--background() !default;
.cwf-tabs {
--cwf-tabs--display: #{$tabs--display};
--cwf-tabs--flex-direction: #{$tabs--flex-direction};
--cwf-tabs--align-items: #{$tabs--align-items};
--cwf-tabs--border-color: #{$tabs--border-color};
--cwf-tabs--active-color: #{$tabs--active-color};
--cwf-tabs--inactive-background-color: #{$tabs--inactive-background-color};
--cwf-tabs--inactive-color: #{$tabs--inactive-color};
--cwf-tabs--accent-color: #{$tabs--accent-color};
display: var(--cwf-tabs--display);
flex-direction: var(--cwf-tabs--flex-direction);
align-items: var(--cwf-tabs--align-items);
@include style__spacing;
// Left
$tabs--left--flex-direction: row !default;
&--left {
@include media__breakpoint {
--cwf-tabs--flex-direction: #{$tabs--left--flex-direction};
}
}
// Left & right
$tabs--vertical--align-items: stretch !default;
&--left,
&--right {
@include media__breakpoint {
--cwf-tabs--align-items: #{$tabs--vertical--align-items};
}
}
// Right
$tabs--right--flex-direction: row-reverse !default;
&--right {
@include media__breakpoint {
--cwf-tabs--flex-direction: #{$tabs--right--flex-direction};
}
}
}
// ---- Nav ----
$tabs__nav--position: relative !default;
$tabs__nav--display: flex !default;
$tabs__nav--justify-content: flex-start !default;
$tabs__nav--width: 100% !default;
$tabs__nav--margin: 0 0 -1px 0 !default;
$tabs__nav--overflow-x: scroll !default;
$tabs__nav--scrollbar--display: none !default;
.cwf-tabs__nav {
--cwf-tabs__nav--position: #{$tabs__nav--position};
--cwf-tabs__nav--display: #{$tabs__nav--display};
--cwf-tabs__nav--justify-content: #{$tabs__nav--justify-content};
--cwf-tabs__nav--width: #{$tabs__nav--width};
--cwf-tabs__nav--margin: #{$tabs__nav--margin};
--cwf-tabs__nav--overflow-x: #{$tabs__nav--overflow-x};
position: var(--cwf-tabs__nav--position);
display: var(--cwf-tabs__nav--display);
justify-content: var(--cwf-tabs__nav--justify-content);
width: var(--cwf-tabs__nav--width);
margin: var(--cwf-tabs__nav--margin);
overflow-x: var(--cwf-tabs__nav--overflow-x);
scrollbar-width: $tabs__nav--scrollbar--display;
&::-webkit-scrollbar {
display: $tabs__nav--scrollbar--display;
}
// ---- Vertical tabs ----
// Left vertical
$tabs__nav--left--margin: 0 -1px 0 0 !default;
.cwf-tabs--left & {
@include media__breakpoint {
--cwf-tabs__nav--margin: #{$tabs__nav--left--margin};
}
}
// Left & right vertical
$tabs__nav--vertical--width: auto !default;
$tabs__nav--vertical--overflow-x: none !default;
.cwf-tabs--left &,
.cwf-tabs--right & {
@include media__breakpoint {
--cwf-tabs__nav--width: #{$tabs__nav--vertical--width};
--cwf-tabs__nav--overflow-x: #{$tabs__nav--vertical--overflow-x};
}
}
// Right vertical
$tabs__nav--right--margin: 0 0 0 -1px !default;
.cwf-tabs--right & {
@include media__breakpoint {
--cwf-tabs__nav--margin: #{$tabs__nav--right--margin};
}
}
// ---- Tabs alignment ----
// Start aligned
$tabs__nav--start--justify-content: flex-start !default;
.cwf-tabs--start & {
@include media__breakpoint {
--cwf-tabs__nav--justify-content: #{$tabs__nav--start--justify-content};
}
}
// Center aligned
$tabs__nav--center--justify-content: center !default;
.cwf-tabs--center & {
@include media__breakpoint {
--cwf-tabs__nav--justify-content: #{$tabs__nav--center--justify-content};
}
}
// End aligned
$tabs__nav--end--justify-content: flex-end !default;
.cwf-tabs--end & {
@include media__breakpoint {
--cwf-tabs__nav--justify-content: #{$tabs__nav--end--justify-content};
}
}
}
// ---- Tabs wrapper ----
$tabs__tabs--display: flex !default;
$tabs__tabs--flex-direction: row !default;
$tabs__tabs--align-self: flex-start !default;
$tabs__tabs--align-items: flex-end !default;
.cwf-tabs__tabs {
--cwf-tabs__tabs--display: #{$tabs__tabs--display};
--cwf-tabs__tabs--flex-direction: #{$tabs__tabs--flex-direction};
--cwf-tabs__tabs--align-self: #{$tabs__tabs--align-self};
--cwf-tabs__tabs--align-items: #{$tabs__tabs--align-items};
display: var(--cwf-tabs__tabs--display);
flex-direction: var(--cwf-tabs__tabs--flex-direction);
align-self: var(--cwf-tabs__tabs--align-self);
align-items: var(--cwf-tabs__tabs--align-items);
// --- Vertical tabs ---
$tabs__tabs--vertical--padding: 0.5rem !default;
// Left
.cwf-tabs--left & {
@include media__breakpoint {
padding-left: $tabs__tabs--vertical--padding;
}
}
// Left & right
$tabs__tabs--vertical--flex-direction: column !default;
$tabs__tabs--vertical--align-self: center !default;
.cwf-tabs--left &,
.cwf-tabs--right & {
@include media__breakpoint {
--cwf-tabs__tabs--flex-direction: #{$tabs__tabs--vertical--flex-direction};
--cwf-tabs__tabs--align-self: #{$tabs__tabs--vertical--align-self};
}
}
// Right
$tabs__tabs--right--align-items: flex-start !default;
.cwf-tabs--right & {
@include media__breakpoint {
--cwf-tabs__tabs--align-items: #{$tabs__tabs--right--align-items};
padding-right: $tabs__tabs--vertical--padding;
}
}
// ---- Tabs alignment ----
// Start aligned
$tabs__tabs--start--align-items: flex-start !default;
.cwf-tabs--start & {
@include media__breakpoint {
--cwf-tabs__tabs--align-self: #{$tabs__tabs--start--align-items};
}
}
// Center aligned
$tabs__tabs--center--align-items: center !default;
.cwf-tabs--center & {
@include media__breakpoint {
--cwf-tabs__tabs--align-self: #{$tabs__tabs--center--align-items};
}
}
// End aligned
$tabs__tabs--end--align-items: flex-end !default;
.cwf-tabs--end & {
@include media__breakpoint {
--cwf-tabs__tabs--align-self: #{$tabs__tabs--end--align-items};
}
}
}
// ---- Tabs ----
$tabs__tab--flex-shrink: 0 !default;
$tabs__tab--max-width: 75% !default;
$tabs__tab--padding: 1rem 1.5rem !default;
$tabs__tab--border-width: 1px !default;
$tabs__tab--border-style: solid !default;
.cwf-tabs__tab {
--cwf-tabs__tab--flex-shrink: #{$tabs__tab--flex-shrink};
--cwf-tabs__tab--max-width: #{$tabs__tab--max-width};
--cwf-tabs__tab--padding: #{$tabs__tab--padding};
--cwf-tabs__tab--border-width: #{$tabs__tab--border-width};
--cwf-tabs__tab--border-style: #{$tabs__tab--border-style};
flex-shrink: var(--cwf-tabs__tab--flex-shrink);
max-width: var(--cwf-tabs__tab--max-width);
padding: var(--cwf-tabs__tab--padding);
border-width: var(--cwf-tabs__tab--border-width);
border-style: var(--cwf-tabs__tab--border-style);
border-color: var(--cwf-tabs__tab--border-color);
background-color: var(--cwf-tabs__tab--background-color);
font-family: theme__font--sans-serif();
color: var(--cwf-tabs__tab--color);
// Left and right vertical tab
$tabs__tab--vertical--max-width: auto !default;
.cwf-tabs--left &,
.cwf-tabs--right & {
@include media__breakpoint {
--cwf-tabs__tab--max-width: #{$tabs__tab--vertical--max-width};
}
}
// Inactive tab
$tabs__tab--inactive--border-color: var(
--cwf-tabs--inactive-background-color
)
var(--cwf-tabs--inactive-background-color) var(--cwf-tabs--border-color)
var(--cwf-tabs--inactive-background-color) !default;
$tabs__tab--inactive--background-color: var(
--cwf-tabs--inactive-background-color
) !default;
$tabs__tab--inactive--color: var(--cwf-tabs--inactive-color) !default;
&[tabindex="-1"] {
--cwf-tabs__tab--border-color: #{$tabs__tab--inactive--border-color};
--cwf-tabs__tab--background-color: #{$tabs__tab--inactive--background-color};
--cwf-tabs__tab--color: #{$tabs__tab--inactive--color};
// Left inactive tab
$tabs__tab--inactive--left--border-color: var(
--cwf-tabs--inactive-background-color
)
var(--cwf-tabs--border-color)
var(--cwf-tabs--inactive-background-color)
var(--cwf-tabs--inactive-background-color) !default;
.cwf-tabs--left & {
@include media__breakpoint {
--cwf-tabs__tab--border-color: #{$tabs__tab--inactive--left--border-color};
}
}
// Left & right inactive tab
$tabs__tab--inactive--vertical--width: 100% !default;
.cwf-tabs--left &,
.cwf-tabs--right & {
@include media__breakpoint {
width: $tabs__tab--inactive--vertical--width;
}
}
// Right inactive tab
$tabs__tab--inactive--right--border-color: var(
--cwf-tabs--inactive-background-color
)
var(--cwf-tabs--inactive-background-color)
var(--cwf-tabs--inactive-background-color)
var(--cwf-tabs--border-color) !default;
.cwf-tabs--right & {
@include media__breakpoint {
--cwf-tabs__tab--border-color: #{$tabs__tab--inactive--right--border-color};
}
}
// Inactive tab hover
$tabs__tab--inactive--hover--background-color: var(
--cwf-tabs--accent-color
) !default;
$tabs__tab--inactive--hover--color: var(
--cwf-tabs--active-color
) !default;
$tabs__tab--inactive--hover--border-color: var(
--cwf-tabs--accent-color
) !default;
&:hover {
--cwf-tabs__tab--border-color: #{$tabs__tab--inactive--hover--border-color};
--cwf-tabs__tab--background-color: #{$tabs__tab--inactive--hover--background-color};
--cwf-tabs__tab--color: #{$tabs__tab--inactive--hover--color};
}
}
// Active tab
// $tabs__tab--active--padding-top: 1.5rem !default;
$tabs__tab--active--padding: 1.5rem 1.5rem 1rem 1.5rem !default;
$tabs__tab--active--border-color: var(--cwf-tabs--border-color)
var(--cwf-tabs--border-color) theme__color--white()
var(--cwf-tabs--border-color) !default;
$tabs__tab--active--background-color: transparent !default;
$tabs__tab--active--color: unset !default;
$tabs__tab--active--position: relative !default;
$tabs__tab--active--border-bottom-color: var(
--cwf-tabs--active-color
) !default;
&:focus,
&:focus:hover,
&[tabindex="0"],
&[tabindex="0"]:hover {
// --cwf-tabs__tab--padding-top: #{$tabs__tab--active--padding-top};
--cwf-tabs__tab--padding: #{$tabs__tab--active--padding};
--cwf-tabs__tab--border-color: #{$tabs__tab--active--border-color};
--cwf-tabs__tab--background-color: #{$tabs__tab--active--background-color};
--cwf-tabs__tab--color: #{$tabs__tab--active--color};
position: $tabs__tab--active--position;
// Left active tab
$tabs__tab--active--left--padding: 1rem 1.5rem 1rem 2rem !default;
$tabs__tab--active--left--border-color: var(--cwf-tabs--border-color)
theme__color--white() var(--cwf-tabs--border-color)
var(--cwf-tabs--border-color) !default;
.cwf-tabs--left & {
@include media__breakpoint {
--cwf-tabs__tab--padding: #{$tabs__tab--active--left--padding};
--cwf-tabs__tab--border-color: #{$tabs__tab--active--left--border-color};
}
}
// Left & right active tab
$tabs__tab--active--vertical--width: calc(100% + 0.5rem) !default;
.cwf-tabs--left &,
.cwf-tabs--right & {
@include media__breakpoint {
width: $tabs__tab--active--vertical--width;
}
}
// Right active tab
$tabs__tab--active--right--padding: 1rem 2rem 1rem 1.5rem !default;
$tabs__tab--active--right--border-color: var(--cwf-tabs--border-color)
var(--cwf-tabs--border-color) var(--cwf-tabs--border-color)
theme__color--white() !default;
.cwf-tabs--right & {
@include media__breakpoint {
--cwf-tabs__tab--padding: #{$tabs__tab--active--right--padding};
--cwf-tabs__tab--border-color: #{$tabs__tab--active--right--border-color};
}
}
// Active tab indicator
$tabs__tab--active--indicator--content: "" !default;
$tabs__tab--active--indicator--position: absolute !default;
$tabs__tab--active--indicator--direction: -1px !default;
$tabs__tab--active--indicator--display: block !default;
$tabs__tab--active--indicator--width: calc(100% + 2px) !default;
$tabs__tab--active--indicator--height: 0.5rem !default;
$tabs__tab--active--indicator--background-color: var(
--cwf-tabs--accent-color
) !default;
&:before {
content: $tabs__tab--active--indicator--content;
position: $tabs__tab--active--indicator--position;
left: $tabs__tab--active--indicator--direction;
top: $tabs__tab--active--indicator--direction;
display: $tabs__tab--active--indicator--display;
width: $tabs__tab--active--indicator--width;
height: $tabs__tab--active--indicator--height;
background-color: $tabs__tab--active--indicator--background-color;
// Left & right active tab indicator
$tabs__tab--active--indicator--vertical--width: 0.5rem !default;
$tabs__tab--active--indicator--vertical--height: calc(
100% + 2px
) !default;
.cwf-tabs--left &,
.cwf-tabs--right & {
@include media__breakpoint {
width: $tabs__tab--active--indicator--vertical--width;
height: $tabs__tab--active--indicator--vertical--height;
}
}
// Right active tab indicator
$tabs__tab--active--indicator--right--left: calc(
100% - 0.5rem + 1px
) !default;
.cwf-tabs--right & {
@include media__breakpoint {
left: $tabs__tab--active--indicator--right--left;
}
}
}
}
// Focused tab
&:focus:before {
@include style__z-index(hidden);
}
}
// ---- Panels ----
.cwf-tabs__panel {
display: var(--cwf-tabs__panel--display);
min-width: var(--cwf-tabs__panel--min-width);
padding: var(--cwf-tabs__panel--padding);
border: var(--cwf-tabs__panel--border);
// Inactive panel
$tabs__panel--inactive--display: none !default;
&[aria-hidden="true"] {
--cwf-tabs__panel--display: #{$tabs__panel--inactive--display};
}
// Active panel
$tabs__panel--active--display: block !default;
$tabs__panel--active--min-width: 100% !default;
$tabs__panel--active--padding: 1.5rem !default;
$tabs__panel--active--border: 1px solid var(--cwf-tabs--border-color) !default;
&[aria-hidden="false"] {
--cwf-tabs__panel--display: #{$tabs__panel--active--display};
--cwf-tabs__panel--min-width: #{$tabs__panel--active--min-width};
--cwf-tabs__panel--padding: #{$tabs__panel--active--padding};
--cwf-tabs__panel--border: #{$tabs__panel--active--border};
// Active panel content
@include style__children;
// Left & right
$tabs__panel--vertical--flex: 1 !default;
$tabs__panel--vertical--min-width: 0 !default;
.cwf-tabs--left &,
.cwf-tabs--right & {
@include media__breakpoint {
--cwf-tabs__panel--min-width: #{$tabs__panel--vertical--min-width};
flex: $tabs__panel--vertical--flex;
}
}
}
// Focused panel
&:focus {
@include style__z-index(content, middle);
}
}
// The default component class
import { Component } from '../../shared/component';
// Check whether reduced motion is enabled locally or globally
import { reducedMotion } from '../../shared/media';
// Provide functionality to all tabs
export class Tabs extends Component {
constructor({
tabs = 'cwf-tabs',
nav = 'cwf-tabs__nav',
tab = 'cwf-tabs__tab',
panel = 'cwf-tabs__panel'
} = {}) {
super();
// Store all selectors
this.selectors = {
tabs,
nav,
tab,
panel
};
// Setup a run flag,...
this.run = false;
// ... a references array,...
this.references = [];
// ... and the current tabs group
this.current = {};
// Finally, bind all relevant functions to the class
this.keyDown = this.keyDown.bind(this);
this.focus = this.focus.bind(this);
this.blur = this.blur.bind(this);
this.interaction = this.interaction.bind(this);
this.hash = this.hash.bind(this);
this.hashchange = this.hashchange.bind(this);
this.store = this.store.bind(this);
}
// Finds the group of a given element
group(element) {
return this.references.find(({ tabs, panels }) => {
return tabs.includes(element) || panels.includes(element);
});
}
// Handler for when a key is pressed while a tab is focused
keyDown(event) {
// Grab the key pressed...
const { key } = event;
// ... and if nots a navigation key, do nothing else
if (
![
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
'Home',
'End'
].includes(key)
)
return;
// Prevent the default behavior
event.preventDefault();
// Grab the current tab...
const { currentTarget: tab } = event;
// ... and the tabs of the current group
const { tabs } = this.current;
// Next, define the first...
const first = 0;
// ... and last indexes
const last = tabs.length - 1;
// Handle first/last navigation
switch (key) {
case 'Home': // Home = Focus first tab
return tabs[first].focus();
case 'End': // End = Focus last tab
return tabs[last].focus();
}
// Get the index of the current,...
const index = tabs.indexOf(tab);
// ... previous,...
let previous = index - 1;
// ... and next tabs
let next = index + 1;
// If the current tab is the first, set the previous index to that of the last tab
if (previous < first) previous = last;
// If the current tab is the last, set the next index to that of the first tab
if (next > last) next = first;
// Define the previous/next arrows
let previousArrow = 'ArrowLeft';
let nextArrow = 'ArrowRight';
// Grab this tab's nav...
const { nav } = this.current;
// ... and if it's horizontal or vertical
const direction = window.getComputedStyle(nav)['overflow-x'];
// If the tabs are displayed vertically,...
if (direction === 'visible') {
// Set the previous/next arrows to up/down respectively
previousArrow = 'ArrowUp';
nextArrow = 'ArrowDown';
}
// Handle previous/next navigation
switch (key) {
case previousArrow: // Left or up arrow = Focus the previous tab
return tabs[previous].focus();
case nextArrow: // Right or down arrow = Focus the next tab
return tabs[next].focus();
}
}
// Returns half of the given number rounded up
half(number) {
return Math.ceil(number) / 2;
}
// Scroll to center a given tab within its nav
scrollToTab(tab) {
// Grab the given tab's nav...
const { nav } = this.group(tab);
// ... and if it doesn't exist or is not scrollable, do nothing else
if (!nav || !nav.scrollWidth) return;
// Next, grab the nav's width...
const { width: navWidth } = nav.getBoundingClientRect();
// ... and the tab's width
const { width: tabWidth } = tab.getBoundingClientRect();
// Next, define how far left we need to scroll to center the tab...
let left = tab.offsetLeft - (this.half(navWidth) - this.half(tabWidth));
// ... and if it's negative, set it to zero
if (!left) left = 0;
// Finally, smooth scroll to center the tab in the nav
return nav.scrollTo({
left,
behavior: reducedMotion({
reduced: 'auto',
noPreference: 'smooth'
})
});
}
// Toggle tab activation
toggleTabs(id) {
// For each tab,...
this.current.tabs.forEach((tab) => {
// ... activate it if its ID matches the given one,...
// ... otherwise, hide it
const match = tab.id === id;
tab.setAttribute('aria-selected', match);
tab.setAttribute('tabindex', match ? 0 : -1);
});
}
// Toggle panel visibility
togglePanels(id) {
// For each panel,...
this.current.panels.forEach((panel) => {
// ... show it if its ID matches the given one,...
// ... otherwise, hide it
const match = panel.id === id;
panel.setAttribute('aria-hidden', !match);
panel.setAttribute('tabindex', match ? 0 : -1);
});
}
// Handler for when a tab is clicked
click({ currentTarget: tab }) {
// Focus the clicked tab
return tab.focus();
}
// Handler for when a tab is focused
focus({ currentTarget: tab }) {
// Grab the tab's ID,...
const { id: tabId } = tab;
// ... and the ID of the panel the tab controls
const panelId = tab.getAttribute('aria-controls');
// Next, find the group this tab belongs to...
const group = this.group(tab);
// ... and store it as the current group
this.current = group;
// Scroll to center th given tab within its nav
this.scrollToTab(tab);
// Next, toggle the tabs and panels of that group
this.toggleTabs(tabId);
this.togglePanels(panelId);
// Finally, begin listening for the tab's key-down event
tab.addEventListener('keydown', this.keyDown);
}
// Handler for when a tab loses focus
blur({ currentTarget: tab }) {
// Stop listening for the tab's key-down event
tab.removeEventListener('keydown', this.keyDown);
}
// Returns whether to add or remove a listener based on run status
get listener() {
return `${this.run ? 'add' : 'remove'}EventListener`;
}
// Binds to tab's interaction (focus/blur) events
interaction(tab) {
tab[this.listener]('click', this.click);
tab[this.listener]('focus', this.focus);
tab[this.listener]('blur', this.blur);
}
// Store a tab group
store(group) {
// Grab the given group's nav,...
const nav = group.querySelector(`.${this.selectors.nav}`);
// ... tabs,...
const tabs = Array.from(
group.querySelectorAll(`.${this.selectors.tab}`)
);
// ... and panels
const panels = Array.from(
group.querySelectorAll(`.${this.selectors.panel}`)
);
// For each tab, handle its focus/blur events
tabs.forEach(this.interaction);
// Finally, add a reference to the group's tabs and panels
this.references.push({
nav,
tabs,
panels
});
}
// Deactivate tab functionality
deactivate({ tabs }) {
tabs.forEach(this.interaction);
}
// Destroy all tabs components
destroy() {
// Set the run flag to false
this.run = false;
// Stop listening for hash change events...
this.hashchange();
// ... and deactivate all tabs
this.references.forEach(this.deactivate);
// Finally, reset the references array...
this.references = [];
// ... and current group object
this.current = {};
}
// Open a tab if the location hash matches a panel's ID
hash(event) {
// Grab the location hash...
const { hash } = window.location,
panelId = hash.substr(1),
// ... and attempt to to grab the corresponding panel
panel = document.getElementById(panelId);
// If not panel was found, do nothing else
if (!panel) return;
// Next, attempt to find the group of the given panel
const group = this.group(panel);
// If no group was found, do nothing else
if (!group) return;
// If an event was triggered, prevent the default behavior
if (event) event.preventDefault();
// Store the current group
this.current = group;
// Get the ID of the tab that control's the given panel
const tabId = panel.getAttribute('aria-labelledby');
// Finally, toggle the tabs and panels of that group...
this.toggleTabs(tabId);
this.togglePanels(panelId);
// ... and focus the panel
panel.focus();
}
// Bind to the document's hash changes
hashchange() {
this.hash();
window[this.listener]('hashchange', this.hash);
}
// Initialize all tabs components
initialize() {
// Set the run flag to true
this.run = true;
// Attempt to grab all tab groups from the page...
const groups = Array.from(
document.querySelectorAll(`.${this.selectors.tabs}`)
);
// ... and if none exist, do nothing else
if (!groups.length) return;
// Finally, store each tab group...
groups.forEach(this.store);
// ... and listen for hash changes
this.hashchange();
}
}
export default Tabs;
The tabs component compartmentalizes content accessible by a tabbed button interface. Inspiration and technical guidance from the W3’s example of tabs with automatic activation.
A tabs component is comprised of one or many tab/panel pairs. A tabs component is a wrapping div.cwf-tabs
element with a nav.cwf-tabs__nav
navigation element containing every tab (button.cwf-tabs__tab
) and every panel (section.cwf-tabs__panel
). A tab is tied to a panel via an aria-controls
and aria-labelledby
attribute respectively.
If the sum of the tab widths exceeds the width of the nav, it will begin horizontally scrolling.
If a modifier class of .cwf-tabs--left
or .cwf-tabs--right
is added to the wrapping div.cwf-tabs
element, it will vertically align the tabs to the left or right respectively. On mobile devices, vertical tabs will revert back to the default (horizontal).
If a modifier class of .cwf-tabs--start
, .cwf-tabs--center
, or .cwf-tabs--end
is added to the wrapping div.cwf-tabs
element, it will align the tabs to the start (left when horizontal, top when vertical), center, or end (right when horizontal, bottom when vertical) of the nav respectively. On mobile devices, the tab alignment will revert back to the default.
Each tab within a tabs component is listening for focus events. When a tab is focused, it will open its corresponding panel and if the nav is scrollable, it will smooth scroll the tab to the center of the nav. It will also begin listening for keydown events when focused for the following keyboard navigation with its group:
In addition, this component is listening for hash changes. If the hash changes to the ID of a panel, it will open and focus it; This allows for linking to a given tabs panel on the same or different page without usability concerns.