<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>"
    }
  ]
}
  • Content:
    // Tabs component styles
    
    // ---- Modules ----
    
    @use "../../shared/media";
    @use "../../shared/style";
    @use "../../shared/theme";
    @use "sass:list";
    
    // Selector prefix
    $prefix: "cwf" !default;
    
    // ---- Wrapper ----
    
    $display: flex !default;
    $flex-direction: column !default;
    $align-items: flex-start !default;
    $border-color: style.lighten("white-darkest", 33%) !default;
    $color--active: theme.accent--foreground() !default;
    $background-color--inactive: style.color("white-dark") !default;
    $color--inactive: style.color("gray-light") !default;
    $color--accent: theme.accent--background() !default;
    
    .#{$prefix}-tabs {
        --cwf-tabs--display: #{$display};
        --cwf-tabs--flex-direction: #{$flex-direction};
        --cwf-tabs--align-items: #{$align-items};
        --cwf-tabs--border-color: #{$border-color};
        --cwf-tabs--active-color: #{$color--active};
        --cwf-tabs--inactive-background-color: #{$background-color--inactive};
        --cwf-tabs--inactive-color: #{$color--inactive};
        --cwf-tabs--accent-color: #{$color--accent};
    
        display: var(--cwf-tabs--display);
        flex-direction: var(--cwf-tabs--flex-direction);
        align-items: var(--cwf-tabs--align-items);
        @include style.spacing;
    
        // Left
    
        $flex-direction--left: row !default;
    
        &--left {
            @include media.breakpoint {
                --cwf-tabs--flex-direction: #{$flex-direction--left};
            }
        }
    
        // Left & right
    
        $align-items--vertical: stretch !default;
    
        &--left,
        &--right {
            @include media.breakpoint {
                --cwf-tabs--align-items: #{$align-items--vertical};
            }
        }
    
        // Right
    
        $flex-direction--right: row-reverse !default;
    
        &--right {
            @include media.breakpoint {
                --cwf-tabs--flex-direction: #{$flex-direction--right};
            }
        }
    }
    
    // ---- Utilities ----
    
    $vertical--directions: left, right !default;
    
    @function vertical--validate($directions) {
        @return $directions == $vertical--directions or
            list.index($vertical--directions, $directions);
    }
    
    @mixin vertical($directions: $vertical--directions) {
        @if not vertical--validate($directions) {
            @warn "#{$directions} is not a valid vertical tab direction! Must be #{$vertical--directions}, or both. Falling back to #{$vertical--directions}.";
            $directions: $vertical--directions;
        }
    
        @if list.length($directions) > 1 {
            .#{$prefix}-tabs--#{list.nth($directions, 1)} &,
            .#{$prefix}-tabs--#{list.nth($directions, 2)} & {
                @include media.breakpoint {
                    @content;
                }
            }
        } @else {
            .#{$prefix}-tabs--#{$directions} & {
                @include media.breakpoint {
                    @content;
                }
            }
        }
    }
    
    $alignment--values: start, center, end !default;
    
    @function alignment--validate($alignment) {
        @return list.index($alignment--values, $alignment);
    }
    
    @mixin alignment($alignment) {
        @if alignment--validate($alignment) {
            .#{$prefix}-tabs--#{$alignment} & {
                @include media.breakpoint {
                    @content;
                }
            }
        } @else {
            @warn "#{$alignment} is not a valid tab alignment value! Must be one of the following: #{$alignment--values}.";
        }
    }
    
    // ---- Nav ----
    
    $nav__position: relative !default;
    $nav__display: flex !default;
    $nav__justify-content: flex-start !default;
    $nav__width: 100% !default;
    $nav__margin: 0 0 -1px 0 !default;
    $nav__overflow-x: scroll !default;
    $nav__scrollbar-display: none !default;
    
    .#{$prefix}-tabs__nav {
        --cwf-tabs__nav--position: #{$nav__position};
        --cwf-tabs__nav--display: #{$nav__display};
        --cwf-tabs__nav--justify-content: #{$nav__justify-content};
        --cwf-tabs__nav--width: #{$nav__width};
        --cwf-tabs__nav--margin: #{$nav__margin};
        --cwf-tabs__nav--overflow-x: #{$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: $nav__scrollbar-display;
    
        &::-webkit-scrollbar {
            display: $nav__scrollbar-display;
        }
    
        // ---- Vertical tabs ----
    
        // Left vertical
    
        $nav__margin--left: 0 -1px 0 0 !default;
    
        @include vertical(left) {
            --cwf-tabs__nav--margin: #{$nav__margin--left};
        }
    
        // Left & right vertical
    
        $nav__width--vertical: auto !default;
        $nav__overflow-x--vertical: none !default;
    
        @include vertical {
            --cwf-tabs__nav--width: #{$nav__width--vertical};
            --cwf-tabs__nav--overflow-x: #{$nav__overflow-x--vertical};
        }
    
        // Right vertical
    
        $nav__margin--right: 0 0 0 -1px !default;
    
        @include vertical(right) {
            --cwf-tabs__nav--margin: #{$nav__margin--right};
        }
    
        // ---- Tabs alignment ----
    
        // Start aligned
    
        $nav__justify-content--start: flex-start !default;
    
        @include alignment(start) {
            --cwf-tabs__nav--justify-content: #{$nav__justify-content--start};
        }
    
        // Center aligned
    
        $nav__justify-content--center: center !default;
    
        @include alignment(center) {
            --cwf-tabs__nav--justify-content: #{$nav__justify-content--center};
        }
    
        // End aligned
    
        $nav__justify-content--end: flex-end !default;
    
        @include alignment(end) {
            --cwf-tabs__nav--justify-content: #{$nav__justify-content--end};
        }
    }
    
    // ---- Tabs wrapper ----
    
    $tabs__display: flex !default;
    $tabs__flex-direction: row !default;
    $tabs__align-self: flex-start !default;
    $tabs__align-items: flex-end !default;
    
    .#{$prefix}-tabs__tabs {
        --cwf-tabs__tabs--display: #{$tabs__display};
        --cwf-tabs__tabs--flex-direction: #{$tabs__flex-direction};
        --cwf-tabs__tabs--align-self: #{$tabs__align-self};
        --cwf-tabs__tabs--align-items: #{$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__padding--vertical: 0.5rem !default;
    
        // Left
    
        @include vertical(left) {
            padding-left: $tabs__padding--vertical;
        }
    
        // Left & right
    
        $tabs__flex-direction--vertical: column !default;
        $tabs__align-self--vertical: center !default;
    
        @include vertical {
            --cwf-tabs__tabs--flex-direction: #{$tabs__flex-direction--vertical};
            --cwf-tabs__tabs--align-self: #{$tabs__align-self--vertical};
        }
    
        // Right
    
        $tabs__align-items--right: flex-start !default;
    
        @include vertical(right) {
            --cwf-tabs__tabs--align-items: #{$tabs__align-items--right};
    
            padding-right: $tabs__padding--vertical;
        }
    
        // ---- Tabs alignment ----
    
        // Start aligned
    
        $tabs__align-items--start: flex-start !default;
    
        @include alignment(start) {
            --cwf-tabs__tabs--align-self: #{$tabs__align-items--start};
        }
    
        // Center aligned
    
        $tabs__align-items--center: center !default;
    
        @include alignment(center) {
            --cwf-tabs__tabs--align-self: #{$tabs__align-items--center};
        }
    
        // End aligned
    
        $tabs__align-items--end: flex-end !default;
    
        @include alignment(end) {
            --cwf-tabs__tabs--align-self: #{$tabs__align-items--end};
        }
    }
    
    // ---- Tabs ----
    
    $tab__flex-shrink: 0 !default;
    $tab__max-width: 75% !default;
    $tab__padding: 1rem 1.5rem !default;
    $tab__border-width: 1px !default;
    $tab__border-style: solid !default;
    
    .#{$prefix}-tabs__tab {
        --cwf-tabs__tab--flex-shrink: #{$tab__flex-shrink};
        --cwf-tabs__tab--max-width: #{$tab__max-width};
        --cwf-tabs__tab--padding: #{$tab__padding};
        --cwf-tabs__tab--border-width: #{$tab__border-width};
        --cwf-tabs__tab--border-style: #{$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
    
        $tab__max-width--vertical: auto !default;
    
        @include vertical {
            --cwf-tabs__tab--max-width: #{$tab__max-width--vertical};
        }
    
        // Inactive tab
    
        $tab__border-color--inactive: var(--cwf-tabs--inactive-background-color)
            var(--cwf-tabs--inactive-background-color) var(--cwf-tabs--border-color)
            var(--cwf-tabs--inactive-background-color) !default;
        $tab__background-color--inactive: var(
            --cwf-tabs--inactive-background-color
        ) !default;
        $tab__color--inactive: var(--cwf-tabs--inactive-color) !default;
    
        &[tabindex="-1"] {
            --cwf-tabs__tab--border-color: #{$tab__border-color--inactive};
            --cwf-tabs__tab--background-color: #{$tab__background-color--inactive};
            --cwf-tabs__tab--color: #{$tab__color--inactive};
    
            // Left inactive tab
    
            $tab__border-color--inactive--left: var(
                    --cwf-tabs--inactive-background-color
                )
                var(--cwf-tabs--border-color)
                var(--cwf-tabs--inactive-background-color)
                var(--cwf-tabs--inactive-background-color) !default;
    
            @include vertical(left) {
                --cwf-tabs__tab--border-color: #{$tab__border-color--inactive--left};
            }
    
            // Left & right inactive tab
    
            $tab__width--inactive--vertical: 100% !default;
    
            @include vertical {
                width: $tab__width--inactive--vertical;
            }
    
            // Right inactive tab
    
            $tab__border-color--inactive--right: var(
                    --cwf-tabs--inactive-background-color
                )
                var(--cwf-tabs--inactive-background-color)
                var(--cwf-tabs--inactive-background-color)
                var(--cwf-tabs--border-color) !default;
    
            @include vertical(right) {
                --cwf-tabs__tab--border-color: #{$tab__border-color--inactive--right};
            }
    
            // Inactive tab hover
    
            $tab__background-color--inactive--hover: var(
                --cwf-tabs--accent-color
            ) !default;
            $tab__color--inactive--hover: var(--cwf-tabs--active-color) !default;
            $tab__border-color--inactive--hover: var(
                --cwf-tabs--accent-color
            ) !default;
    
            &:hover {
                --cwf-tabs__tab--border-color: #{$tab__border-color--inactive--hover};
                --cwf-tabs__tab--background-color: #{$tab__background-color--inactive--hover};
                --cwf-tabs__tab--color: #{$tab__color--inactive--hover};
            }
        }
    
        // Active tab
    
        $tab__padding--active: 1.5rem 1.5rem 1rem 1.5rem !default;
        $tab__border-color--active: var(--cwf-tabs--border-color)
            var(--cwf-tabs--border-color) theme.color--white()
            var(--cwf-tabs--border-color) !default;
        $tab__background-color--active: transparent !default;
        $tab__color--active: unset !default;
        $tab__position--active: relative !default;
        $tab__border-bottom-color--active: var(--cwf-tabs--active-color) !default;
    
        &:focus,
        &:focus:hover,
        &[tabindex="0"],
        &[tabindex="0"]:hover {
            --cwf-tabs__tab--padding: #{$tab__padding--active};
            --cwf-tabs__tab--border-color: #{$tab__border-color--active};
            --cwf-tabs__tab--background-color: #{$tab__background-color--active};
            --cwf-tabs__tab--color: #{$tab__color--active};
    
            position: $tab__position--active;
    
            // Left active tab
    
            $tab__padding--active--left: 1rem 1.5rem 1rem 2rem !default;
            $tab__border-color--active--left: var(--cwf-tabs--border-color)
                theme.color--white() var(--cwf-tabs--border-color)
                var(--cwf-tabs--border-color) !default;
    
            @include vertical(left) {
                --cwf-tabs__tab--padding: #{$tab__padding--active--left};
                --cwf-tabs__tab--border-color: #{$tab__border-color--active--left};
            }
    
            // Left & right active tab
    
            $tab__width--active--vertical: calc(100% + 0.5rem) !default;
    
            @include vertical {
                width: $tab__width--active--vertical;
            }
    
            // Right active tab
    
            $tab__padding--active--right: 1rem 2rem 1rem 1.5rem !default;
            $tab__border-color--active--right: var(--cwf-tabs--border-color)
                var(--cwf-tabs--border-color) var(--cwf-tabs--border-color)
                theme.color--white() !default;
    
            @include vertical(right) {
                --cwf-tabs__tab--padding: #{$tab__padding--active--right};
                --cwf-tabs__tab--border-color: #{$tab__border-color--active--right};
            }
    
            // Active tab indicator
    
            $tab--indicator__content--active: "" !default;
            $tab--indicator__position--active: absolute !default;
            $tab--indicator__direction--active: -1px !default;
            $tab--indicator__display--active: block !default;
            $tab--indicator__width--active: calc(100% + 2px) !default;
            $tab--indicator__height--active: 0.5rem !default;
            $tab--indicator__background-color--active: var(
                --cwf-tabs--accent-color
            ) !default;
    
            &:before {
                content: $tab--indicator__content--active;
                position: $tab--indicator__position--active;
                left: $tab--indicator__direction--active;
                top: $tab--indicator__direction--active;
                display: $tab--indicator__display--active;
                width: $tab--indicator__width--active;
                height: $tab--indicator__height--active;
                background-color: $tab--indicator__background-color--active;
    
                // Left & right active tab indicator
    
                $tab--indicator__width--active--vertical: 0.5rem !default;
                $tab--indicator__height--active--vertical: calc(
                    100% + 2px
                ) !default;
    
                @include vertical {
                    width: $tab--indicator__width--active--vertical;
                    height: $tab--indicator__height--active--vertical;
                }
    
                // Right active tab indicator
    
                $tab--indicator__left--active--right: calc(
                    100% - 0.5rem + 1px
                ) !default;
    
                @include vertical(right) {
                    left: $tab--indicator__left--active--right;
                }
            }
        }
    
        // Focused tab
    
        &:focus:before {
            @include style.z-index("hidden");
        }
    }
    
    // ---- Panels ----
    
    .#{$prefix}-tabs__panel {
        display: var(--cwf-tabs__panel--display);
        min-width: var(--cwf-tabs__panel--min-width);
        max-width: var(--cwf-tabs__panel--max-width);
        padding: var(--cwf-tabs__panel--padding);
        border: var(--cwf-tabs__panel--border);
    
        // Inactive panel
    
        $panel__display--inactive: none !default;
    
        &[aria-hidden="true"] {
            --cwf-tabs__panel--display: #{$panel__display--inactive};
        }
    
        // Active panel
    
        $panel__display--active: block !default;
        $panel__width--active: 100% !default;
        $panel__padding--active: 1.5rem !default;
        $panel__border--active: 1px solid var(--cwf-tabs--border-color) !default;
    
        &[aria-hidden="false"] {
            --cwf-tabs__panel--display: #{$panel__display--active};
            --cwf-tabs__panel--min-width: #{$panel__width--active};
            --cwf-tabs__panel--max-width: #{$panel__width--active};
            --cwf-tabs__panel--padding: #{$panel__padding--active};
            --cwf-tabs__panel--border: #{$panel__border--active};
    
            // Active panel content
    
            @include style.children;
    
            // Left & right
    
            $panel__flex--vertical: 1 !default;
            $panel__min-width--vertical: 0 !default;
    
            @include vertical {
                --cwf-tabs__panel--min-width: #{$panel__min-width--vertical};
    
                flex: $panel__flex--vertical;
            }
        }
    }
    
  • URL: /components/raw/tabs/_index.scss
  • Filesystem Path: components/tabs/_index.scss
  • Size: 16 KB
  • Content:
    // The default component class
    import { Component } from '../../shared/component.js';
    
    // Check whether reduced motion is enabled locally or globally
    import { reducedMotion } from '../../shared/media/index.js';
    
    // Find focusable descendants and toggle an element's tab order
    import {
        descendants as getFocusableDescendants,
        toggle as toggleTabOrder
    } from '../../shared/focus.js';
    
    // Provide functionality to all tabs
    export class Tabs extends Component {
        constructor({
            prefix = 'cwf',
            tabs = 'tabs',
            nav = 'tabs__nav',
            tab = 'tabs__tab',
            panel = 'tabs__panel'
        } = {}) {
            super({
                prefix,
                classes: {
                    tabs,
                    nav,
                    tab,
                    panel
                }
            });
    
            // Initialize an object for 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.find((tab) => tab === element) ||
                    panels.find(({ panel }) => panel === 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);
                toggleTabOrder(match, tab);
            });
        }
    
        // Toggle panel visibility
        togglePanels(id) {
            // For each panel,...
            this.current.panels.forEach(({ panel, focusables }) => {
                // ... show it if its ID matches the given one,...
                // ... otherwise, hide it
                const match = panel.id === id;
                panel.setAttribute('aria-hidden', !match);
                toggleTabOrder(match, panel, ...focusables);
            });
        }
    
        // 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);
        }
    
        // 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 and their focusable decendants
            const panels = Array.from(
                group.querySelectorAll(this.selectors.panel)
            ).map((panel) => {
                // Get the panel's focusable elements...
                const focusables = getFocusableDescendants(panel);
                // ... and if the panel is hidden, remove its focusable elements from the tab order
                if (panel.getAttribute('tabindex') === '-1')
                    toggleTabOrder(false, ...focusables);
    
                // Finally, return the panel and its focusable elements
                return {
                    panel,
                    focusables
                };
            });
    
            // 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);
        }
    
        // Open a tab if the location hash matches a panel's ID
        hash(event) {
            // Grab the location hash...
            const id = window.location.hash.substring(1);
            // ... and attempt to to grab the corresponding panel
            const panel = document.getElementById(id);
    
            // 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 tab = panel.getAttribute('aria-labelledby');
    
            // Finally, toggle the tabs and panels of that group...
            this.toggleTabs(tab);
            this.togglePanels(id);
            // ... and focus the panel
            panel.focus();
        }
    
        // Bind to the document's hash changes
        hashchange() {
            this.hash();
            window[this.listener]('hashchange', this.hash);
        }
    
        // Mount/unmount tabs functionality
        mount(run) {
            super.mount(run);
    
            // If mounting,...
            if (run) {
                // ... attempt to grab all tab groups from the page,...
                const groups = Array.from(
                    document.querySelectorAll(this.selectors.tabs)
                );
                // ... do nothing else if none exist,...
                if (!groups.length) return;
                // ... or store each tab group...
                groups.forEach(this.store);
            } else {
                // ... otherwise, deactivate all references...
                this.references.forEach(this.deactivate.bind(this));
                // ... and reset all references and the current tab group
                this.references = [];
                this.current = {};
            }
    
            // Finally, start/stop listening to hash changes
            this.hashchange();
        }
    }
    
  • URL: /components/raw/tabs/index.js
  • Filesystem Path: components/tabs/index.js
  • Size: 10.6 KB

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.

Markup

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.

Vertical tabs

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).

Tab alignment

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.

Javascript

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:

  • Home - Open the first tab’s panel.
  • Left arrow or up arrow - Open the previous tab’s panel. If the current tab is the first, open the last tab’s panel. (Left = horizontal tab alignment / Up = vertical tab alignment)
  • Right arrow or down arrow - Open the next tab’s panel. If the current tab is the last, open the first tab’s panel. (Right = horizontal tab alignment / Down = vertical tab alignment)
  • End - Open the last tab’s panel

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.

T4 implementation

Tabs are implemented in T4 as the “Tab Panel” plugin, meaning its classes are .plugin- prefixed instead of .cwf- prefixed.

The plugin outputs a single tab/panel pair, and using 1 or more sequential plugins group them together as an entire tabs component.

Areas

Global or local areas are not supported by this plugin, meaning this plugin can only be used where normal content goes (e.g. the text/html content layout).

Injectors

In the “Injectors” field of the “Tab Panel” plugin, the following injectors can be used:

  • id:{custom_id} - Overrides the default, T4 ID of the tab panel with a custom ID.
  • vertical:{left|right} - Vertically aligns the tabs to the left or the right on the entire tabs group. This injector only works on the first tab panel of the group.
  • alignment:{start|center|end} - Aligns the tabs to the start, center, or end of the nav on the entire tabs group. This injector only works on the first tab panel of the group.
  • class:{custom_classes} - Adds custom classes to the tab panel.
  • style:{custom_styles} - Adds custom styles to a style attribute of the tab panel.
  • before:{custom_html} - Adds custom HTML before the tab panel.
  • after:{custom_html} - Adds custom HTML after the tab panel.