Build an Accessible Tablist

Are you in the right place? If you are constrained to a third-party component library, see the Tab Accessibility Checklist and ensure that it passes. Otherwise, follow the guidance here to choose the best option for your project.


Alternatives

For projects not ready to adopt the Design System Whether you seek to remediate an existing component or build one from scratch, these reference implementations will ensure an accessible outcome. For more details, read about this pattern.

Custom Tablist (first demo)

When to use If your team has source-level control over UI code, and either can't use Web Components or have a pressing need for unique functionality not provided by the NYSDS Tab component, you may choose to build a custom tablist. Or perhaps your legacy component has been flagged for remediation and you need a high quality reference.

How is it different This tablist expresses the look-and-feel of the design system with many aspects modeled after the NYSDS Tab component. The ways in which it differs show how to honor the spirit of the design system when alternative accessible interaction and layout features are implemented. Specifically, when tab headers scroll horizontally, this tablist shows scroll buttons with large, generous targets to ease use with touch interfaces (such as tablets) and pointing devices (such as mice) by people with motor disabilities.

About the first demo On this page there are two demos of the same component. This first demo has limited content and thus appears simple.

  • In this demo the tabset gains its accessible name by being aria-labelledby the h4 heading above it
  • The scroll buttons, which will not appear in this example, are nevertheless aria-describedby the same heading
  • The tabset has an optional, custom CSS class to set its height and margin
Rely on the required checklists to ensure accessibility is maintained during integration.

Some notable 20th-century French classical composers

Georges Auric (15 February 1899 – 23 July 1983) was a French composer, born in Lodève, Hérault, France. He was considered one of Les Six—The Group of Six—a group of artists informally associated with Jean Cocteau and Erik Satie. Before he turned 20 he had orchestrated and written incidental music for several ballets and stage productions. He also had a long and distinguished career as a film composer.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. In sit amet massa auctor quam viverra malesuada. Maecenas eu lorem a ex ullamcorper rhoncus non vitae eros. Morbi tincidunt egestas lacus, eget consectetur nibh vestibulum aliquam. Nulla erat quam, hendrerit id congue suscipit, luctus ultricies urna. Nullam erat nisl, posuere id blandit non, porta eget justo. Integer faucibus sem ac maximus auctor. Aliquam et orci tempus, volutpat nisl sed, rhoncus eros. Praesent egestas felis risus, id feugiat purus ullamcorper a. Nunc malesuada, erat sit amet interdum fringilla, magna est iaculis diam, vitae vehicula mi elit eu erat. Praesent congue tortor ultrices, sagittis risus eget, suscipit orci. Vivamus eu vestibulum mauris. Suspendisse vel diam mauris. Sed ante enim, eleifend vel quam in, dignissim porttitor ipsum. Nunc suscipit felis nec elit blandit sollicitudin.

Vivamus ac porttitor risus. Nulla convallis, libero at consectetur viverra, nulla dolor tempus ipsum, quis iaculis orci lacus quis ex. Fusce ac turpis vehicula, efficitur quam sit amet, tristique lectus. Duis quis semper lectus. Vestibulum semper ligula et congue vestibulum. Quisque laoreet lacinia sapien ut faucibus. Suspendisse id molestie lectus. Etiam faucibus nec lacus sit amet egestas. Mauris quis congue mi, posuere pellentesque dolor. Mauris eget sodales nibh. Vestibulum ultrices quam id finibus euismod. Curabitur eleifend lorem non fringilla feugiat.

Maecenas efficitur et lorem sit amet bibendum. Sed a molestie odio, nec iaculis velit. Duis ut porta quam. Etiam aliquam fermentum erat, sed consequat neque viverra quis. Etiam vitae ante consequat, pretium nulla et, tempus risus. Cras eleifend ex mauris, sit amet faucibus velit fermentum vel. Integer malesuada, sapien vitae pellentesque lobortis, erat ligula viverra tellus, quis scelerisque nunc risus nec arcu. Quisque tincidunt tempor risus ac pulvinar. Aenean efficitur massa eu leo iaculis sagittis in sit amet velit. Sed egestas enim efficitur, facilisis tortor varius, fringilla tellus. Morbi pellentesque eleifend nulla, in malesuada sapien molestie id. Cras pharetra erat lectus, at mattis arcu imperdiet eget.

Aliquam ullamcorper mauris ut scelerisque ultrices. Maecenas dapibus, elit vitae interdum finibus, ligula velit interdum enim, id iaculis quam tortor non justo. In dapibus elit sed dolor tempus tempus. Quisque nunc nisi, suscipit a tellus a, accumsan varius arcu. Morbi interdum erat non auctor tempus. Praesent viverra dolor vel rutrum semper. Suspendisse iaculis massa sit amet mauris commodo faucibus. Etiam tempor velit dolor, vitae lobortis sem lacinia at. Nam viverra sapien eget mi suscipit tincidunt sed semper sapien. Maecenas ut turpis eu ligula ultrices dapibus. Nullam facilisis ipsum vitae rutrum laoreet. Donec euismod pretium tempor. Nullam velit ex, ullamcorper in consectetur ut, accumsan ac nulla. Suspendisse sem velit, elementum vitae leo et, suscipit volutpat urna.

Proin mattis eros vel nulla dapibus, id placerat tortor convallis. In a velit vel ex vestibulum egestas quis sed massa. Vestibulum eu nibh vel ex bibendum consectetur. Quisque sed purus ut mauris gravida bibendum. Ut dui nibh, volutpat nec tincidunt sed, laoreet vel purus. Vestibulum varius purus in justo laoreet, eu scelerisque tortor mollis. Proin efficitur ullamcorper magna, at finibus nisl fringilla id. Vivamus viverra vulputate quam sed aliquam. Nullam massa quam, viverra eget dictum vel, cursus vitae libero. Morbi lacinia, nisi quis blandit tristique, urna ex volutpat lectus, eget rutrum libero dolor ut arcu. Cras porta purus ac convallis mattis.

Copy Code
<style>
    .tab-custom-1 {
        height: 360px;
        margin-block-end: 2rem;
    }
</style>

<h4 id="Tab1Title">
    Some notable 20th-century French classical composers
</h4>
<div
    aria-labelledby="Tab1Title"
    class="nysa11y tablist tab-custom-1"
    data-component="tablist"
    data-html="custom"
    role="tablist">

    <div data-part="tabs">
        <div data-part="scrollButtons">
            <button aria-describedby="Tab1Title" data-scroll="to-start" type="button">
                <span class="sr-only">visual scroll toward start</span>
            </button>
            <button aria-describedby="Tab1Title" data-scroll="to-end" type="button">
                <span class="sr-only">visual scroll toward end</span>
            </button>
        </div>

        <div data-part="tabsEnclosure">

            <!-- ==================================== -->
            <i class="tabs__scroll-affordance-start"></i>
            <!-- ==================================== -->

            <div data-part="scrollContainer">

                <!-- ======================== -->
                <s data-part="sentinelStart"></s>
                <!-- ======================== -->

                <!-- //////// Tab state and associations.
                | ---------------------------------------
                | role="tab"        REQUIRED: hardcode
                | aria-controls=""  Managed by JavaScript
                | aria-selected=""  Managed by JavaScript
                | id=""             Managed by JavaScript
                | tabindex=""       Managed by JavaScript
                | type="button"     Recommended: hardcode
                | ---------------------------------------
                | Inspect with DevTools. \\\\\\\\\\\\ -->

                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Georges Auric</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Claude Debussy</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Gabriel Fauré</button>

                <!-- ====================== -->
                <s data-part="sentinelEnd"></s>
                <!-- ====================== -->

            </div>

            <!-- ================================== -->
            <i class="tabs__scroll-affordance-end"></i>
            <!-- ================================== -->

        </div>
    </div>

    <div data-part="tabpanels" tabindex="0">

        <!-- ///// Tabpanel state and associations.
        | -----------------------------------------
        | role="tabpanel"     REQUIRED: hardcode
        | aria-labelledby=""  Managed by JavaScript
        | id=""               Managed by JavaScript
        | hidden              Managed by JavaScript
        | -----------------------------------------
        | Inspect with DevTools. \\\\\\\\\\\\\\ -->

        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Georges_Auric"
                    lang="fr">Georges Auric</a>
                (15 February 1899 – 23 July 1983) was a French
                composer, born in
                <span lang="fr">Lodève, Hérault,</span> France. He
                was considered one of
                <span lang="fr">Les Six</span>—The Group of Six—a
                group of artists informally associated with
                <span lang="fr">Jean Cocteau</span> and
                <span lang="fr">Erik Satie</span>. Before he turned
                20 he had orchestrated and written incidental music
                for several ballets and stage productions. He also
                had a long and distinguished career as a film
                composer.
            </p>
            <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing
                elit. In sit amet massa auctor quam viverra
                malesuada. Maecenas eu lorem a ex ullamcorper
                rhoncus non vitae eros. Morbi tincidunt egestas
                lacus, eget consectetur nibh vestibulum aliquam.
                Nulla erat quam, hendrerit id congue suscipit,
                luctus ultricies urna. Nullam erat nisl, posuere id
                blandit non, porta eget justo. Integer faucibus sem
                ac maximus auctor. Aliquam et orci tempus, volutpat
                nisl sed, rhoncus eros. Praesent egestas felis
                risus, id feugiat purus ullamcorper a. Nunc
                malesuada, erat sit amet interdum fringilla, magna
                est iaculis diam, vitae vehicula mi elit eu erat.
                Praesent congue tortor ultrices, sagittis risus
                eget, suscipit orci. Vivamus eu vestibulum mauris.
                Suspendisse vel diam mauris. Sed ante enim, eleifend
                vel quam in, dignissim porttitor ipsum. Nunc
                suscipit felis nec elit blandit sollicitudin.
            </p>
            <p>
                Vivamus ac porttitor risus. Nulla convallis, libero
                at consectetur viverra, nulla dolor tempus ipsum,
                quis iaculis orci lacus quis ex. Fusce ac turpis
                vehicula, efficitur quam sit amet, tristique lectus.
                Duis quis semper lectus. Vestibulum semper ligula et
                congue vestibulum. Quisque laoreet lacinia sapien ut
                faucibus. Suspendisse id molestie lectus. Etiam
                faucibus nec lacus sit amet egestas. Mauris quis
                congue mi, posuere pellentesque dolor. Mauris eget
                sodales nibh. Vestibulum ultrices quam id finibus
                euismod. Curabitur eleifend lorem non fringilla
                feugiat.
            </p>
            <p>
                Maecenas efficitur et lorem sit amet bibendum. Sed a
                molestie odio, nec iaculis velit. Duis ut porta
                quam. Etiam aliquam fermentum erat, sed consequat
                neque viverra quis. Etiam vitae ante consequat,
                pretium nulla et, tempus risus. Cras eleifend ex
                mauris, sit amet faucibus velit fermentum vel.
                Integer malesuada, sapien vitae pellentesque
                lobortis, erat ligula viverra tellus, quis
                scelerisque nunc risus nec arcu. Quisque tincidunt
                tempor risus ac pulvinar. Aenean efficitur massa eu
                leo iaculis sagittis in sit amet velit. Sed egestas
                enim efficitur, facilisis tortor varius, fringilla
                tellus. Morbi pellentesque eleifend nulla, in
                malesuada sapien molestie id. Cras pharetra erat
                lectus, at mattis arcu imperdiet eget.
            </p>
            <p>
                Aliquam ullamcorper mauris ut scelerisque ultrices.
                Maecenas dapibus, elit vitae interdum finibus,
                ligula velit interdum enim, id iaculis quam tortor
                non justo. In dapibus elit sed dolor tempus tempus.
                Quisque nunc nisi, suscipit a tellus a, accumsan
                varius arcu. Morbi interdum erat non auctor tempus.
                Praesent viverra dolor vel rutrum semper.
                Suspendisse iaculis massa sit amet mauris commodo
                faucibus. Etiam tempor velit dolor, vitae lobortis
                sem lacinia at. Nam viverra sapien eget mi suscipit
                tincidunt sed semper sapien. Maecenas ut turpis eu
                ligula ultrices dapibus. Nullam facilisis ipsum
                vitae rutrum laoreet. Donec euismod pretium tempor.
                Nullam velit ex, ullamcorper in consectetur ut,
                accumsan ac nulla. Suspendisse sem velit, elementum
                vitae leo et, suscipit volutpat urna.
            </p>
            <p>
                Proin mattis eros vel nulla dapibus, id placerat
                tortor convallis. In a velit vel ex vestibulum
                egestas quis sed massa. Vestibulum eu nibh vel ex
                bibendum consectetur. Quisque sed purus ut mauris
                gravida bibendum. Ut dui nibh, volutpat nec
                tincidunt sed, laoreet vel purus. Vestibulum varius
                purus in justo laoreet, eu scelerisque tortor
                mollis. Proin efficitur ullamcorper magna, at
                finibus nisl fringilla id. Vivamus viverra vulputate
                quam sed aliquam. Nullam massa quam, viverra eget
                dictum vel, cursus vitae libero. Morbi lacinia, nisi
                quis blandit tristique, urna ex volutpat lectus,
                eget rutrum libero dolor ut arcu. Cras porta purus
                ac convallis mattis.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Claude_Debussy"
                    lang="fr">Claude Debussy</a>
                (22 August 1862 – 25 March 1918) was a French
                composer. He is sometimes seen as the first
                Impressionist composer, although he vigorously
                rejected the term. He was among the most influential
                composers of the late 19th and early 20th centuries.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Gabriel_Fauré"
                    lang="fr">Gabriel Fauré</a>
                (12 May 1845 – 4 November 1924) was a French
                composer, organist, pianist and teacher. He was one
                of the foremost French composers of his generation,
                and his musical style influenced many 20th-century
                composers. Among his best-known works are his
                <span lang="fr">Pavane, Requiem, Sicilienne,</span>
                nocturnes for piano and the songs
                <span lang="fr">"Après un rêve"</span> and
                <span lang="fr">"Clair de lune"</span>. Although his
                best-known and most accessible compositions are
                generally his earlier ones,
                <span lang="fr">Fauré</span> composed many of his
                most highly regarded works in his later years, in a
                more harmonically and melodically complex style.
            </p>
        </div>
    </div>
</div>

Custom Tablist (second demo)

About the second demo This demo has a large number of tabs in order to force scrolling, as well as a footer.

  • In this demo the tabset gains its accessible name from an aria-label set on the component div
  • The scroll buttons, which do appear in this example, are aria-describedby the ID of the component div
  • The scroll buttons change state depending on the extent of scroll in either direction. When changes occur there are multiple affordances (mouse, keyboard, screen reader), particularly when a button becomes aria-disabled
  • Footer content height subtracts from whatever explicit height is assigned the tabset.
Rely on the required checklists to ensure accessibility is maintained during integration.

Georges Auric (15 February 1899 – 23 July 1983) was a French composer, born in Lodève, Hérault, France. He was considered one of Les Six—The Group of Six—a group of artists informally associated with Jean Cocteau and Erik Satie. Before he turned 20 he had orchestrated and written incidental music for several ballets and stage productions. He also had a long and distinguished career as a film composer.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. In sit amet massa auctor quam viverra malesuada. Maecenas eu lorem a ex ullamcorper rhoncus non vitae eros. Morbi tincidunt egestas lacus, eget consectetur nibh vestibulum aliquam. Nulla erat quam, hendrerit id congue suscipit, luctus ultricies urna. Nullam erat nisl, posuere id blandit non, porta eget justo. Integer faucibus sem ac maximus auctor. Aliquam et orci tempus, volutpat nisl sed, rhoncus eros. Praesent egestas felis risus, id feugiat purus ullamcorper a. Nunc malesuada, erat sit amet interdum fringilla, magna est iaculis diam, vitae vehicula mi elit eu erat. Praesent congue tortor ultrices, sagittis risus eget, suscipit orci. Vivamus eu vestibulum mauris. Suspendisse vel diam mauris. Sed ante enim, eleifend vel quam in, dignissim porttitor ipsum. Nunc suscipit felis nec elit blandit sollicitudin.

Vivamus ac porttitor risus. Nulla convallis, libero at consectetur viverra, nulla dolor tempus ipsum, quis iaculis orci lacus quis ex. Fusce ac turpis vehicula, efficitur quam sit amet, tristique lectus. Duis quis semper lectus. Vestibulum semper ligula et congue vestibulum. Quisque laoreet lacinia sapien ut faucibus. Suspendisse id molestie lectus. Etiam faucibus nec lacus sit amet egestas. Mauris quis congue mi, posuere pellentesque dolor. Mauris eget sodales nibh. Vestibulum ultrices quam id finibus euismod. Curabitur eleifend lorem non fringilla feugiat.

Maecenas efficitur et lorem sit amet bibendum. Sed a molestie odio, nec iaculis velit. Duis ut porta quam. Etiam aliquam fermentum erat, sed consequat neque viverra quis. Etiam vitae ante consequat, pretium nulla et, tempus risus. Cras eleifend ex mauris, sit amet faucibus velit fermentum vel. Integer malesuada, sapien vitae pellentesque lobortis, erat ligula viverra tellus, quis scelerisque nunc risus nec arcu. Quisque tincidunt tempor risus ac pulvinar. Aenean efficitur massa eu leo iaculis sagittis in sit amet velit. Sed egestas enim efficitur, facilisis tortor varius, fringilla tellus. Morbi pellentesque eleifend nulla, in malesuada sapien molestie id. Cras pharetra erat lectus, at mattis arcu imperdiet eget.

Aliquam ullamcorper mauris ut scelerisque ultrices. Maecenas dapibus, elit vitae interdum finibus, ligula velit interdum enim, id iaculis quam tortor non justo. In dapibus elit sed dolor tempus tempus. Quisque nunc nisi, suscipit a tellus a, accumsan varius arcu. Morbi interdum erat non auctor tempus. Praesent viverra dolor vel rutrum semper. Suspendisse iaculis massa sit amet mauris commodo faucibus. Etiam tempor velit dolor, vitae lobortis sem lacinia at. Nam viverra sapien eget mi suscipit tincidunt sed semper sapien. Maecenas ut turpis eu ligula ultrices dapibus. Nullam facilisis ipsum vitae rutrum laoreet. Donec euismod pretium tempor. Nullam velit ex, ullamcorper in consectetur ut, accumsan ac nulla. Suspendisse sem velit, elementum vitae leo et, suscipit volutpat urna.

Proin mattis eros vel nulla dapibus, id placerat tortor convallis. In a velit vel ex vestibulum egestas quis sed massa. Vestibulum eu nibh vel ex bibendum consectetur. Quisque sed purus ut mauris gravida bibendum. Ut dui nibh, volutpat nec tincidunt sed, laoreet vel purus. Vestibulum varius purus in justo laoreet, eu scelerisque tortor mollis. Proin efficitur ullamcorper magna, at finibus nisl fringilla id. Vivamus viverra vulputate quam sed aliquam. Nullam massa quam, viverra eget dictum vel, cursus vitae libero. Morbi lacinia, nisi quis blandit tristique, urna ex volutpat lectus, eget rutrum libero dolor ut arcu. Cras porta purus ac convallis mattis.

When a Tablist is assigned a height value, if a footer is present as a direct child of the tablist, the footer will steal height from the tabpanel.

Copy Code
<style>
    .tab-custom-2 {
        height: 420px;
        margin-block: 1rem;
    }
</style>
<div
    aria-label="A partial list of French composers active in the 20th century"
    class="nysa11y tablist tab-custom-2"
    data-component="tablist"
    data-html="custom"
    role="tablist"
    id="TabTwo">

    <div data-part="tabs">
        <div data-part="scrollButtons">
            <button aria-describedby="TabTwo" data-scroll="to-start" type="button">
                <span class="sr-only">visual scroll toward start</span>
            </button>
            <button aria-describedby="TabTwo" data-scroll="to-end" type="button">
                <span class="sr-only">visual scroll toward end</span>
            </button>
        </div>

        <div data-part="tabsEnclosure">

            <!-- ==================================== -->
            <i class="tabs__scroll-affordance-start"></i>
            <!-- ==================================== -->

            <div data-part="scrollContainer">

                <!-- ======================== -->
                <s data-part="sentinelStart"></s>
                <!-- ======================== -->

                <!-- //////// Tab state and associations.
                | ---------------------------------------
                | role="tab"        REQUIRED: hardcode
                | aria-controls=""  Managed by JavaScript
                | aria-selected=""  Managed by JavaScript
                | id=""             Managed by JavaScript
                | tabindex=""       Managed by JavaScript
                | type="button"     Recommended: hardcode
                | ---------------------------------------
                | Inspect with DevTools. \\\\\\\\\\\\ -->

                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Georges Auric</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Claude Debussy</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Gabriel Fauré</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Jean Françaix</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">André Jolivet</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Charles Koechlin</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Darius Milhaud</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Francis Poulenc</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Erik Satie</button>
                <button
                    role="tab"
                    aria-controls=""
                    aria-selected=""
                    id=""
                    tabindex=""
                    type="button"
                    lang="fr">Henri Sauguet</button>

                <!-- ====================== -->
                <s data-part="sentinelEnd"></s>
                <!-- ====================== -->

            </div>

            <!-- ================================== -->
            <i class="tabs__scroll-affordance-end"></i>
            <!-- ================================== -->

        </div>
    </div>

    <div data-part="tabpanels" tabindex="0">

    <!-- ///// Tabpanel state and associations.
    | -----------------------------------------
    | role="tabpanel"     REQUIRED: hardcode
    | aria-labelledby=""  Managed by JavaScript
    | id=""               Managed by JavaScript
    | hidden              Managed by JavaScript
    | -----------------------------------------
    | Inspect with DevTools. \\\\\\\\\\\\\\ -->

        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Georges_Auric"
                    lang="fr">Georges Auric</a>
                (15 February 1899 – 23 July 1983) was a French
                composer, born in
                <span lang="fr">Lodève, Hérault,</span> France. He
                was considered one of
                <span lang="fr">Les Six</span>—The Group of Six—a
                group of artists informally associated with
                <span lang="fr">Jean Cocteau</span> and
                <span lang="fr">Erik Satie</span>. Before he turned
                20 he had orchestrated and written incidental music
                for several ballets and stage productions. He also
                had a long and distinguished career as a film
                composer.
            </p>
            <p>
                Lorem ipsum dolor sit amet, consectetur adipiscing
                elit. In sit amet massa auctor quam viverra
                malesuada. Maecenas eu lorem a ex ullamcorper
                rhoncus non vitae eros. Morbi tincidunt egestas
                lacus, eget consectetur nibh vestibulum aliquam.
                Nulla erat quam, hendrerit id congue suscipit,
                luctus ultricies urna. Nullam erat nisl, posuere id
                blandit non, porta eget justo. Integer faucibus sem
                ac maximus auctor. Aliquam et orci tempus, volutpat
                nisl sed, rhoncus eros. Praesent egestas felis
                risus, id feugiat purus ullamcorper a. Nunc
                malesuada, erat sit amet interdum fringilla, magna
                est iaculis diam, vitae vehicula mi elit eu erat.
                Praesent congue tortor ultrices, sagittis risus
                eget, suscipit orci. Vivamus eu vestibulum mauris.
                Suspendisse vel diam mauris. Sed ante enim, eleifend
                vel quam in, dignissim porttitor ipsum. Nunc
                suscipit felis nec elit blandit sollicitudin.
            </p>
            <p>
                Vivamus ac porttitor risus. Nulla convallis, libero
                at consectetur viverra, nulla dolor tempus ipsum,
                quis iaculis orci lacus quis ex. Fusce ac turpis
                vehicula, efficitur quam sit amet, tristique lectus.
                Duis quis semper lectus. Vestibulum semper ligula et
                congue vestibulum. Quisque laoreet lacinia sapien ut
                faucibus. Suspendisse id molestie lectus. Etiam
                faucibus nec lacus sit amet egestas. Mauris quis
                congue mi, posuere pellentesque dolor. Mauris eget
                sodales nibh. Vestibulum ultrices quam id finibus
                euismod. Curabitur eleifend lorem non fringilla
                feugiat.
            </p>
            <p>
                Maecenas efficitur et lorem sit amet bibendum. Sed a
                molestie odio, nec iaculis velit. Duis ut porta
                quam. Etiam aliquam fermentum erat, sed consequat
                neque viverra quis. Etiam vitae ante consequat,
                pretium nulla et, tempus risus. Cras eleifend ex
                mauris, sit amet faucibus velit fermentum vel.
                Integer malesuada, sapien vitae pellentesque
                lobortis, erat ligula viverra tellus, quis
                scelerisque nunc risus nec arcu. Quisque tincidunt
                tempor risus ac pulvinar. Aenean efficitur massa eu
                leo iaculis sagittis in sit amet velit. Sed egestas
                enim efficitur, facilisis tortor varius, fringilla
                tellus. Morbi pellentesque eleifend nulla, in
                malesuada sapien molestie id. Cras pharetra erat
                lectus, at mattis arcu imperdiet eget.
            </p>
            <p>
                Aliquam ullamcorper mauris ut scelerisque ultrices.
                Maecenas dapibus, elit vitae interdum finibus,
                ligula velit interdum enim, id iaculis quam tortor
                non justo. In dapibus elit sed dolor tempus tempus.
                Quisque nunc nisi, suscipit a tellus a, accumsan
                varius arcu. Morbi interdum erat non auctor tempus.
                Praesent viverra dolor vel rutrum semper.
                Suspendisse iaculis massa sit amet mauris commodo
                faucibus. Etiam tempor velit dolor, vitae lobortis
                sem lacinia at. Nam viverra sapien eget mi suscipit
                tincidunt sed semper sapien. Maecenas ut turpis eu
                ligula ultrices dapibus. Nullam facilisis ipsum
                vitae rutrum laoreet. Donec euismod pretium tempor.
                Nullam velit ex, ullamcorper in consectetur ut,
                accumsan ac nulla. Suspendisse sem velit, elementum
                vitae leo et, suscipit volutpat urna.
            </p>
            <p>
                Proin mattis eros vel nulla dapibus, id placerat
                tortor convallis. In a velit vel ex vestibulum
                egestas quis sed massa. Vestibulum eu nibh vel ex
                bibendum consectetur. Quisque sed purus ut mauris
                gravida bibendum. Ut dui nibh, volutpat nec
                tincidunt sed, laoreet vel purus. Vestibulum varius
                purus in justo laoreet, eu scelerisque tortor
                mollis. Proin efficitur ullamcorper magna, at
                finibus nisl fringilla id. Vivamus viverra vulputate
                quam sed aliquam. Nullam massa quam, viverra eget
                dictum vel, cursus vitae libero. Morbi lacinia, nisi
                quis blandit tristique, urna ex volutpat lectus,
                eget rutrum libero dolor ut arcu. Cras porta purus
                ac convallis mattis.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Claude_Debussy"
                    lang="fr">Claude Debussy</a>
                (22 August 1862 – 25 March 1918) was a French
                composer. He is sometimes seen as the first
                Impressionist composer, although he vigorously
                rejected the term. He was among the most influential
                composers of the late 19th and early 20th centuries.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Gabriel_Fauré"
                    lang="fr">Gabriel Fauré</a>
                (12 May 1845 – 4 November 1924) was a French
                composer, organist, pianist and teacher. He was one
                of the foremost French composers of his generation,
                and his musical style influenced many 20th-century
                composers. Among his best-known works are his
                <span lang="fr">Pavane, Requiem, Sicilienne,</span>
                nocturnes for piano and the songs
                <span lang="fr">"Après un rêve"</span> and
                <span lang="fr">"Clair de lune"</span>. Although his
                best-known and most accessible compositions are
                generally his earlier ones,
                <span lang="fr">Fauré</span> composed many of his
                most highly regarded works in his later years, in a
                more harmonically and melodically complex style.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Jean_Françaix"
                    lang="fr">Jean Françaix</a>
                (23 May 1912 – 25 September 1997) was a French
                neoclassical composer, pianist, and orchestrator
                known for his prolific output and vibrant style.
                <span lang="fr">Françaix</span> composed for various
                genres, and is particularly known for his chamber
                works for piano as well as winds.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/André_Jolivet"
                    lang="fr">André Jolivet</a>
                (8 August 1905 – 20 December 1974) was a French
                composer. Known for his devotion to French culture
                and musical thought,
                <span lang="fr">Jolivet</span> drew on his interest
                in acoustics and atonality, as well as both ancient
                and modern musical influences, particularly on
                instruments used in ancient times. He composed in a
                wide variety of forms for many different types of
                ensembles.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Charles_Koechlin"
                    lang="fr">Charles Koechlin</a>
                (27 November 1867 – 31 December 1950), commonly
                known as <span lang="fr">Charles Koechlin</span>,
                was a French composer, teacher and musicologist.
                Among his better known works is
                <span lang="fr">Les Heures persanes</span>, a set of
                piano pieces based on the novel
                <span lang="fr">Vers Ispahan</span> by
                <span lang="fr">Pierre Loti</span> and The Seven
                Stars Symphony, a 7 movement symphony where each
                movement is themed around a different film star (all
                Silent era stars) who were popular at the time of
                the piece's writing (1933).
            </p>
            <p>
                He was a political radical all his life and a
                passionate enthusiast for such diverse things as
                medieval music, The Jungle Book of Rudyard Kipling,
                <span lang="de">Johann Sebastian Bach</span>, film
                stars (especially Lilian Harvey and Ginger Rogers),
                traveling, stereoscopic photography and socialism.
                He once said: "The artist needs an ivory tower, not
                as an escape from the world, but as a place where he
                can view the world and be himself. This tower is for
                the artist like a lighthouse shining out across the
                world."
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Darius_Milhaud"
                    lang="fr">Darius Milhaud</a>
                (4 September 1892 – 22 June 1974) was a French
                composer, conductor, and teacher. He was a member of
                <span lang="fr">Les Six</span>—The Group of Six—and
                one of the most prolific composers of the 20th
                century. His compositions are influenced by jazz and
                Brazilian music and make extensive use of
                polytonality. Milhaud is considered one of the key
                modernist composers. He taught many future jazz and
                classical composers, including Burt Bacharach, Dave
                Brubeck, Philip Glass, Steve Reich,
                <span lang="hu">György Kurtág</span>,
                <span lang="de">Karlheinz Stockhausen</span> and
                <span lang="el">Iannis Xenakis</span> among others.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Francis_Poulenc"
                    lang="fr">Francis Poulenc</a>
                (7 January 1899 – 30 January 1963) was a French
                composer and pianist. His compositions include
                songs, solo piano works, chamber music, choral
                pieces, operas, ballets, and orchestral concert
                music. Among the best-known are the piano suite
                <span lang="fr">Trois mouvements perpétuels</span>
                (1919), the ballet
                <span lang="fr">Les biches</span> (1923), the
                <span lang="fr">Concert champêtre</span> (1928) for
                harpsichord and orchestra, the Organ Concerto
                (1938), the opera
                <span lang="fr">Dialogues des Carmélites</span>
                (1957), and the Gloria (1959) for soprano, choir,
                and orchestra.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Erik Satie"
                    lang="fr">Erik Alfred Leslie Satie</a>
                (17 May 1866 – 1 July 1925), better known as
                <span lang="fr">Erik Satie</span>, was a French
                composer and pianist. The son of a French father and
                a British mother, he studied at the Paris
                Conservatoire but was undistinguished and did not
                obtain a diploma. In the 1880s he worked as a
                pianist in café-cabarets in
                <span lang="fr">Montmartre</span>, Paris, and began
                composing works, mostly for solo piano, such as his
                <span lang="fr">Gymnopédies</span> and
                <span lang="fr">Gnossiennes</span>. He also wrote
                music for a Rosicrucian sect to which he was briefly
                attached.
            </p>
        </div>
        <div
            role="tabpanel"
            aria-labelledby=""
            id=""
            hidden
        >
            <p>
                <a
                    href="https://en.wikipedia.org/wiki/Henri_Sauguet"
                    lang="fr">Henri Sauguet</a>
                (18 May 1901 – 22 June 1989) was a French composer.
                Born in <span lang="fr">Bordeaux</span>, he adopted
                his mother's maiden name as part of his professional
                pseudonym. His output includes operas, ballets, four
                symphonies (1945, 1949, 1955, 1971), concertos,
                chamber and choral music and numerous songs, as well
                as film music. Although he experimented with
                <span lang="fr">musique concrète</span> and expanded
                tonality, he remained opposed to particular systems
                and his music evolved little: he developed tonal or
                modal ideas in smooth curves, producing an art of
                clarity, simplicity and restraint.
            </p>
        </div>
    </div>
    <div data-part="tabfooter">
        <h3>Footer</h3>
        <p>
            When a Tablist is assigned a height value, if a footer is present as a direct child of the tablist,
            the footer will steal height from the tabpanel.<br>
        </p>
    </div>
</div>
Copy Code
/**
 * NYSA11y Tablist
 * Path: /assets/nysa11y/tablist-custom.css
 * Depends on /assets/nysa11y/tablist-custom.js
 */
@layer nysa11y {
    body:has(.nysa11y) {
        div.nysa11y.tablist[data-component="tablist"][data-html="custom"] {
            --nysa11y-divider-thickness: 3px;
            *,
            *:before,
            *:after {
                box-sizing: border-box;
            }
            font-family:
                "Proxima Nova",
                -apple-system,
                "BlinkMacSystemFont",
                "Segoe UI",
                "Roboto",
                "Helvetica",
                "Arial",
                sans-serif;
            display: flex;
            flex-direction: column;

            & [data-part="tabs"] {
                background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 3 3' width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h3v3H0z' fill='%23ededed'/%3E%3C/svg%3E%0A");
                background-position: bottom;
                background-repeat: repeat-x;
                display: flex;
                & [data-part="scrollButtons"] {
                    & button[data-scroll] {
                        background-color: var(
                            --nys-color-theme-weaker,
                            #eff6fb
                        );
                        border-color: var(--nys-color-theme-weak, #cddde9);
                        background-position: center;
                        background-repeat: no-repeat;
                        border-radius: 0;
                        height: 100%;
                        min-width: 44px;
                        border-inline: none;
                        border-block-start: none;
                        border-block-end-width: 3px;
                        border-start-end-radius: var(--nys-radius-md, 4px);
                        border-start-start-radius: var(--nys-radius-md, 4px);
                        border-end-start-radius: 0;
                        border-end-end-radius: 0;
                        margin-inline-end: 1px;
                        &:focus-visible {
                            outline: solid var(--nys-border-width-md, 2px)
                                var(--nys-color-focus, #004dd1);
                            outline-offset: calc(
                                var(--nys-space-2px, 2px) * -1
                            );
                        }
                        &[data-scroll="to-start"] {
                            background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline fill='%23154973' points='14.724 12.486, 7.375 19.835, 7.013 20.710, 7.400 21.585, 8.275 21.960, 9.150 21.585, 16.849 13.911, 17.298 13.236, 17.448 12.486, 17.298 11.737, 16.849 11.062, 9.150 3.363, 8.262 3.000, 7.375 3.388, 7.000 4.263, 7.375 5.138, 14.724 12.486'/%3E%3C/svg%3E%0A");
                            background-size: contain;
                            transform: scaleX(-1);
                            &[aria-disabled="true"] {
                                background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline fill='gray' points='14.724 12.486, 7.375 19.835, 7.013 20.710, 7.400 21.585, 8.275 21.960, 9.150 21.585, 16.849 13.911, 17.298 13.236, 17.448 12.486, 17.298 11.737, 16.849 11.062, 9.150 3.363, 8.262 3.000, 7.375 3.388, 7.000 4.263, 7.375 5.138, 14.724 12.486'/%3E%3C/svg%3E%0A");
                            }
                        }
                        &[data-scroll="to-end"] {
                            background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline fill='%23154973' points='14.724 12.486, 7.375 19.835, 7.013 20.710, 7.400 21.585, 8.275 21.960, 9.150 21.585, 16.849 13.911, 17.298 13.236, 17.448 12.486, 17.298 11.737, 16.849 11.062, 9.150 3.363, 8.262 3.000, 7.375 3.388, 7.000 4.263, 7.375 5.138, 14.724 12.486'/%3E%3C/svg%3E%0A");
                            background-size: contain;
                            &[aria-disabled="true"] {
                                background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline fill='gray' points='14.724 12.486, 7.375 19.835, 7.013 20.710, 7.400 21.585, 8.275 21.960, 9.150 21.585, 16.849 13.911, 17.298 13.236, 17.448 12.486, 17.298 11.737, 16.849 11.062, 9.150 3.363, 8.262 3.000, 7.375 3.388, 7.000 4.263, 7.375 5.138, 14.724 12.486'/%3E%3C/svg%3E%0A");
                            }
                        }
                        &[aria-disabled="true"] {
                            background-color: var(
                                --nys-color-neutral-10,
                                #f6f6f6
                            );
                            border-color: transparent;
                            pointer-events: none;
                            &:focus-visible {
                                outline: dashed var(--nys-border-width-md, 2px)
                                    var(--nys-color-neutral-100, #d0d0ce);
                            }
                        }
                    }
                }
                & [data-part="tabsEnclosure"] {
                    position: relative;
                    overflow: hidden;
                    display: flex;
                    & .tabs__scroll-affordance-start,
                    & .tabs__scroll-affordance-end {
                        height: calc(100% - var(--nysa11y-divider-thickness));
                        pointer-events: none;
                        position: absolute;
                        width: var(--nys-space-200, 16px);
                        z-index: 10;
                        &[class$="-start"] {
                            background-image: linear-gradient(
                                to left,
                                transparent,
                                var(--nys-color-neutral-100, #d0d0ce)
                            );
                            left: 0px;
                        }
                        &[class$="-end"] {
                            background-image: linear-gradient(
                                to right,
                                transparent,
                                var(--nys-color-neutral-100, #d0d0ce)
                            );
                            right: 0px;
                        }
                    }

                    & [data-part="scrollContainer"] {
                        display: flex;
                        overflow-x: auto;
                        scroll-behavior: smooth;
                        scrollbar-width: none;

                        & [data-part^="sentinel"] {
                            height: 1px;
                            width: 1px;
                            flex-shrink: 0;
                            flex-grow: 0;
                        }

                        & button[role="tab"] {
                            background-color: var(--nys-color-surface, #ffffff);
                            border-color: var(--nys-color-neutral-50, #ededed);
                            border-start-end-radius: var(--nys-radius-md, 4px);
                            border-start-start-radius: var(
                                --nys-radius-md,
                                4px
                            );
                            border-style: none none solid;
                            border-width: var(--nysa11y-divider-thickness);
                            color: var(--nys-color-ink, #1b1b1b);
                            cursor: pointer;
                            display: grid;
                            font-family: var(
                                --nys-font-family-ui,
                                Proxima Nova,
                                -apple-system,
                                BlinkMacSystemFont,
                                Segoe UI,
                                Roboto,
                                Helvetica,
                                Arial,
                                sans-serif,
                                Apple Color Emoji,
                                Segoe UI Emoji,
                                Segoe UI Symbol
                            );
                            font-size: var(--nys-font-size-ui-md, 16px);
                            font-weight: var(--nys-font-weight-semibold, 600);
                            line-height: var(--nys-size-200, 16px);
                            margin-inline-end: 8px;
                            padding: var(--nys-space-200, 16px)
                                var(--nys-space-150, 12px);
                            place-content: center;
                            white-space: nowrap;
                            &:last-of-type {
                                margin-inline-end: 1px;
                            }
                            &:focus-visible {
                                outline: solid var(--nys-border-width-md, 2px)
                                    var(--nys-color-focus, #004dd1);
                                outline-offset: calc(
                                    var(--nys-space-2px, 2px) * -1
                                );
                            }
                            &:hover {
                                background-color: var(
                                    --nys-color-theme-weaker,
                                    #eff6fb
                                );
                                border-color: var(
                                    --nys-color-theme-weak,
                                    #cddde9
                                );
                            }
                            &[aria-selected="true"] {
                                background-color: var(
                                    --nys-color-neutral-10,
                                    #f6f6f6
                                );
                                border-color: var(--nys-color-theme, #154973);
                                &:hover {
                                    background-color: var(
                                        --nys-color-theme-weaker,
                                        #eff6fb
                                    );
                                    border-color: var(
                                        --nys-color-theme-strong,
                                        #0e324f
                                    );
                                }
                            }
                        }
                    }
                }
            }

            & [data-part="tabpanels"] {
                flex-grow: 1;
                margin: 1rem;
                overflow: auto;
                overscroll-behavior: none;
                padding: 1rem;
                &:focus-visible {
                    outline: solid var(--nys-border-width-md, 2px)
                        var(--nys-color-focus, #004dd1);
                }
                & [role="tabpanel"] {
                    /* customize per content */
                }
            }

            & [data-part="tabfooter"] {
                /* customize per content */
            }
        }

        /* Non-Tablist classes
           Placed here, these take effect both inside and outside the Tablist */
        a:link:focus-visible {
            outline: solid var(--nys-border-width-md, 2px)
                var(--nys-color-focus, #004dd1);
            outline-offset: var(--nys-space-2px, 2px);
        }
        .sr-only {
            border: 0 !important;
            clip: rect(1px, 1px, 1px, 1px) !important;
            -webkit-clip-path: inset(50%) !important;
            clip-path: inset(50%) !important;
            height: 1px !important;
            overflow: hidden !important;
            margin: -1px !important;
            padding: 0 !important;
            position: absolute !important;
            width: 1px !important;
            white-space: nowrap !important;
        }
    }
}
Copy Code
/*
 * NYSA11y Tablist
 * Path: /assets/nysa11y/tablist-custom.js
 * Depends on /assets/nysa11y/tablist-custom.css
 */
const nysa11y = window.nysa11y || {};

class Tablist {
  #selectorTablist = '[data-component="tablist"].nysa11y.tablist';
  #selectorTab = '[role="tab"]';
  #selectorPanel = '[role="tabpanel"]';
  #selectorScrollButtons = '[data-part="scrollButtons"]';
  #selectorScrollContainer = '[data-part="scrollContainer"]';
  #selectorTabsEnclosure = '[data-part="tabsEnclosure"]';
  #selectorSentinelStart = '[data-part="sentinelStart"]';
  #selectorSentinelEnd = '[data-part="sentinelEnd"]';
  #selectorScrollBtnStart = '[data-scroll="to-start"]';
  #selectorScrollBtnEnd = '[data-scroll="to-end"]';
  #selectorOverlayStart = ".tabs__scroll-affordance-start";
  #selectorOverlayEnd = ".tabs__scroll-affordance-end";
  #scrollStep = 300;
  // Element-level expando used to disconnect a prior IntersectionObserver
  // before wiring a new one when init() runs more than once.
  #observerKey = "__nysa11yTablistObserver";

  constructor(options = {}) {
    this.container = options.container || document;
    this.init();
  }

  init() {
    const tablists = this.container.querySelectorAll(this.#selectorTablist);
    if (!tablists.length) return;
    tablists.forEach((tablistEl) => {
      this.#ensureIds(tablistEl);
      this.#setupTabs(tablistEl);
      this.#setupScrollControls(tablistEl);
    });
  }

  // Generate id / aria-controls / aria-labelledby only when missing, so
  // hand-authored ids survive and ids stay unique across simultaneous tablists.
  #ensureIds = (tablistEl) => {
    const tabs = tablistEl.querySelectorAll(this.#selectorTab);
    const panels = tablistEl.querySelectorAll(this.#selectorPanel);
    tabs.forEach((tab, index) => {
      const panel = panels[index];
      const ariaControls = tab.getAttribute("aria-controls");
      const linkedPanel = ariaControls
        ? tablistEl.querySelector(`#${CSS.escape(ariaControls)}`)
        : null;
      if (tab.id && ariaControls && linkedPanel) return;
      const uid = crypto.randomUUID();
      const tabId = `tab-${uid}`;
      const panelId = `panel-${uid}`;
      tab.id = tabId;
      tab.setAttribute("aria-controls", panelId);
      if (panel) {
        panel.id = panelId;
        panel.setAttribute("aria-labelledby", tabId);
      }
    });
  };

  #setupTabs = (tablistEl) => {
    const tabs = tablistEl.querySelectorAll(this.#selectorTab);
    if (!tabs.length) return;
    let presetSelected = null;
    tabs.forEach((tab) => {
      tab.removeEventListener("click", this.#handleClick);
      tab.addEventListener("click", this.#handleClick);
      tab.removeEventListener("keydown", this.#handleKeydown);
      tab.addEventListener("keydown", this.#handleKeydown);
      if (!presetSelected && tab.getAttribute("aria-selected") === "true") {
        presetSelected = tab;
      }
    });
    this.#selectTab(tablistEl, presetSelected || tabs[0]);
  };

  #selectTab = (tablistEl, targetTab) => {
    const tabs = tablistEl.querySelectorAll(this.#selectorTab);
    tabs.forEach((tab) => {
      const isSelected = tab === targetTab;
      tab.setAttribute("aria-selected", isSelected ? "true" : "false");
      if (isSelected) {
        tab.removeAttribute("tabindex");
      } else {
        tab.tabIndex = -1;
      }
      const ariaControls = tab.getAttribute("aria-controls");
      if (!ariaControls) return;
      const panel = tablistEl.querySelector(`#${CSS.escape(ariaControls)}`);
      if (!panel) return;
      panel.hidden = !isSelected;
    });
  };

  #handleClick = (event) => {
    const tab = event.currentTarget;
    const tablistEl = tab.closest(this.#selectorTablist);
    if (!tablistEl) return;
    this.#selectTab(tablistEl, tab);
    tab.focus();
  };

  #handleKeydown = (event) => {
    const tab = event.currentTarget;
    const tablistEl = tab.closest(this.#selectorTablist);
    if (!tablistEl) return;
    const tabs = Array.from(tablistEl.querySelectorAll(this.#selectorTab));
    const idx = tabs.indexOf(tab);
    if (idx < 0) return;
    let next = null;
    switch (event.key) {
      case "ArrowLeft":
        next = tabs[(idx - 1 + tabs.length) % tabs.length];
        break;
      case "ArrowRight":
        next = tabs[(idx + 1) % tabs.length];
        break;
      case "Home":
        next = tabs[0];
        break;
      case "End":
        next = tabs[tabs.length - 1];
        break;
      default:
        return;
    }
    next.focus();
    event.preventDefault();
    event.stopPropagation();
  };

  #setupScrollControls = (tablistEl) => {
    const scrollButtons = tablistEl.querySelector(this.#selectorScrollButtons);
    const scrollContainer = tablistEl.querySelector(
      this.#selectorScrollContainer,
    );
    const tabsEnclosure = tablistEl.querySelector(this.#selectorTabsEnclosure);
    const sentinelStart = tablistEl.querySelector(this.#selectorSentinelStart);
    const sentinelEnd = tablistEl.querySelector(this.#selectorSentinelEnd);
    const scrollStartBtn = tablistEl.querySelector(
      this.#selectorScrollBtnStart,
    );
    const scrollEndBtn = tablistEl.querySelector(this.#selectorScrollBtnEnd);
    const overlayStart = tablistEl.querySelector(this.#selectorOverlayStart);
    const overlayEnd = tablistEl.querySelector(this.#selectorOverlayEnd);

    if (
      !scrollButtons ||
      !scrollContainer ||
      !tabsEnclosure ||
      !sentinelStart ||
      !sentinelEnd
    ) {
      return;
    }

    const states = { sentinelStart: true, sentinelEnd: true };

    const prev = tablistEl[this.#observerKey];
    if (prev && typeof prev.disconnect === "function") prev.disconnect();

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          const isVisible = entry.isIntersecting;
          if (entry.target === sentinelStart) {
            states.sentinelStart = isVisible;
            if (overlayStart) {
              overlayStart.style.display = isVisible ? "none" : "block";
            }
            if (scrollStartBtn) scrollStartBtn.ariaDisabled = isVisible;
          }
          if (entry.target === sentinelEnd) {
            states.sentinelEnd = isVisible;
            if (overlayEnd) {
              overlayEnd.style.display = isVisible ? "none" : "block";
            }
            if (scrollEndBtn) scrollEndBtn.ariaDisabled = isVisible;
          }
        });
        const hasOverflow = !states.sentinelStart || !states.sentinelEnd;
        scrollButtons.style.display = hasOverflow ? "flex" : "none";
      },
      { root: tabsEnclosure, threshold: 0.1 },
    );

    observer.observe(sentinelStart);
    observer.observe(sentinelEnd);
    tablistEl[this.#observerKey] = observer;

    if (scrollStartBtn) {
      scrollStartBtn.removeEventListener("click", this.#handleScrollClick);
      scrollStartBtn.addEventListener("click", this.#handleScrollClick);
    }
    if (scrollEndBtn) {
      scrollEndBtn.removeEventListener("click", this.#handleScrollClick);
      scrollEndBtn.addEventListener("click", this.#handleScrollClick);
    }
  };

  #handleScrollClick = (event) => {
    const button = event.currentTarget;
    const tablistEl = button.closest(this.#selectorTablist);
    if (!tablistEl) return;
    const scrollContainer = tablistEl.querySelector(
      this.#selectorScrollContainer,
    );
    if (!scrollContainer) return;
    const direction = button.dataset.scroll === "to-start" ? -1 : 1;
    scrollContainer.scrollBy({
      left: direction * this.#scrollStep,
      behavior: "smooth",
    });
  };
}

nysa11y.Tablist = Tablist;

document.addEventListener("DOMContentLoaded", () => {
  new nysa11y.Tablist();
});

About this pattern

A tablist (ARIA role="tablist") contains and associates a set of tab content selectors (ARIA role="tab") and tabpanel content items (ARIA role="tabpanel") with a display area. All tabs are present at all times. Only one tabpanel may appear in the display area at a time; the others are hidden until invoked by activation of a tab. Tablists are not used for navigation, though sometimes network activity may be needed to populate content. Rather, they are used to "shuffle" or "swap" content chunks in the display area. There is no native HTML tablist element; the name is taken from the ARIA role.

Tab and Tabpanel

The display of a tabpanel is controlled by activation of the tab that labels the content. Tabs are button-like and option-like, and as a group they are list-like. Responsibilities of a tab include being able to take focus, to reflect selection state, and to be activated. Tabpanels may contain any valid HTML content. Tabs and tabpanels are typically closely co-located both visually and in code.

Success of the component depends primarily on effective interaction with a tab and the resulting changes in state to the tab and its paired tabpanel. At any given time only one of the set is presented. This state of being selected must be communicated visually, semantically, aurally, and programmatically with affordances:

  • A selected tab is visually distinct
  • When reached by a screen reader all tabs must announce their names, but when selected must also identify as such
  • The tab must respond to its visible name when invoked by voice control systems
  • While keyboard navigation between tabs occurs by Arrow, movement between tab and tabpanel occurs by Tab and Shift + Tab.

For assistive technologies most affordances depend on correct use of ARIA:

  • If the tablist component has a visible label, such as an h3 element, associate them with aria-labelledby="ID". Otherwise, set an aria-label="descriptive string".
  • Ensure each tab aria-controls="ID" its associated tabpanel.
  • Dynamically set the active tab to aria-selected="true" and the others to false.
  • Each tabpanel must be aria-labelledby="ID" its associated tab.
  • When the set of tabs is vertical in visual presentation, the parent tablist requires aria-orientation="vertical".
Best Practices

Screen reader users benefit from tab announcements that include the total number of tabs in the parent tablist. Screen reader and keyboard users benefit from placing the tabpanel perimeter in focus order with tabindex="0". It is safer to require manual rather than automatic loading of tabpanel content. This makes selection more explicitly determined by user choice and guards against performance problems with heavy tabpanel content.

The NYSDS Tab component implements all of the above.

NYS Digital Accessibility MS Teams channel
Have a quick question or need immediate guidance? Jump into our Microsoft Teams channel to chat with the team in real-time.

Report a bug on GitHub
Spotted an issue or a visual glitch? Open a GitHub ticket with a clear description and reproduction steps so we can squash it.