Naively Programmable Module

    @nrk/core-tabs

    2.2.1 • Public • Published

    Core Tabs

    @nrk/core-tabs converts <button> and <a> elements to keyboard accessible tabs, controlling following tabpanels. Tabs can be nested and easily extended with custom animations or behaviour through the tabs.toggle event.

    Examples (plain JS)

    Nested tabs

    <!--demo-->
    <core-tabs>
      <button>Tab 1</button>
      <button>Tab 2</button>
      <a href="#link">Tab 3</a>
    </core-tabs>
    <div>Tabpanel 1</div>
    <div hidden>
      <core-tabs>
        <button>Subtab 1</button>
        <button>Subtab 2</button>
        <button>Subtab 3</button>
      </core-tabs>
      <div hidden>Subtabpanel 1</div>
      <div>Subtabpanel 2</div>
      <div hidden>Subtabpanel 3</div>
    </div>
    <div hidden>Tabpanel 3</div>

    Few panels

    <!--demo-->
    <core-tabs id="few-panels-plain-js" tab="fppj-tab-3">
      <button type="button" data-for="panel-1" id="fppj-tab-1">First tab</button>
      <button type="button" data-for="panel-2" id="fppj-tab-2">Second tab</button>
      <button type="button" data-for="panel-2" id="fppj-tab-3">Third tab</button>
    </core-tabs>
    <div id="panel-1" hidden>
      Text of the first panel
    </div>
    <div id="panel-2">Text of the second panel, shared by second and third tab</div>

    Single panel

    <!--demo-->
    <core-tabs id="single-panel-plain-js" tab='sppj-tab-2'>
      <button type="button" data-for="only-panel" id="sppj-tab-1">First tab</button>
      <button type="button" data-for="only-panel" id="sppj-tab-2">Second tab</button>
      <button type="button" data-for="only-panel" id="sppj-tab-3">Third tab</button>
    </core-tabs>
    <div id="only-panel">Text of the only panel. Note that <code>aria-labelledBy</code> reflects active tab</div>

    Examples (React)

    Nested tabs

    <!--demo-->
    <div id="react-nested-tabs" class="my-vertical-tabs"></div>
    <script type="text/jsx">
      ReactDOM.render(<div>
        <CoreTabs>
          <button>Vertical tab 1 JSX</button>
          <button>Vertical tab 2 JSX</button>
        </CoreTabs>
        <div>Tabpanel 1 JSX</div>
        <div hidden>
          <CoreTabs>
            <button>Subtab 1 JSX</button>
            <button hidden>Subtab 2 JSX</button>
          </CoreTabs>
          <div>Subtabpanel 1</div>
          <div hidden>Subtabpanel 2</div>
        </div>
      </div>, document.getElementById('react-nested-tabs'))
    </script>

    Dynamic tabs

    <!--demo-->
    <div id="react-dynamic-tabs" class="my-vertical-tabs"></div>
    <script type="text/jsx">
      const Dynamic = () => {
          const [elements, setElements] = React.useState([])
          const menu = elements.map(item => <button type="button">Dynamic Tab {item}</button>);
          const pages = elements.map(item => <div hidden>Tabpanel {item}</div>);
    
          return (
            <>
              <button type="button" onClick={() => setElements([...elements, elements.length + 1])}>
                Add extra tab
              </button>
              <button type="button" onClick={() => setElements([1,2])}>
                Set to two tabs
              </button>
              <button type="button" onClick={() => setElements([])}>
                Remove all
              </button>
              <CoreTabs>
                {menu}
              </CoreTabs>
              {pages}
            </>
          )
        }
      ReactDOM.render(<Dynamic />, document.getElementById('react-dynamic-tabs'))
    </script>

    Active by index

    <!--demo-->
    <div id="react-index-tabs"></div>
    <script type="text/jsx">
      const IndexTabs = () => {
          const [tabIndex, setTabIndex] = React.useState(0)
          const handleTabsToggle = (event) => { setTabIndex(parseInt(event.target.getAttribute('tab'))) }
          const handleInputChange = (event) => { setTabIndex(parseInt(event.target.value)) }
    
          return (
            <>
              <div>
                <label>
                  Set active tab
                  <input
                    type="range"
                    value={tabIndex}
                    min="0"
                    max="3"
                    step="1"
                    onChange={handleInputChange}
                  />
                </label>
              </div>
              <CoreTabs tab={tabIndex} onTabsToggle={handleTabsToggle}>
                <button type="button">Tab 1</button>
                <button type="button">Tab 2</button>
                <button type="button">Tab 3</button>
                <button type="button">Tab 4</button>
              </CoreTabs>
              <div>Tabpanel 1</div>
              <div hidden>Tabpanel 2</div>
              <div hidden>Tabpanel 3</div>
              <div hidden>Tabpanel 4</div>
            </>
          )
        }
      ReactDOM.render(<IndexTabs />, document.getElementById('react-index-tabs'))
    </script>

    Single panel

    <!--demo-->
    <div id="react-single-panel"></div>
    <script type="text/jsx">
      const EpisodeList = ({seasonNumber}) => {
        const episodes = [
          ["S1: Episode 1", "S1: Episode 2", "S1: Episode 3"],
          ["S2: Episode 1", "S2: Episode 2"]
        ];
    
        const [episodesInSeason, setEpisodesInSeason] = React.useState(episodes[seasonNumber - 1]);
    
        React.useEffect(() => {
          setEpisodesInSeason(episodes[seasonNumber - 1])
        }, [seasonNumber])
    
        return (
          <div id="episode-list">
            Table panel for season {seasonNumber}: Content here should change: 
            <br/><br/>
            {episodesInSeason.map(episode => <button>{episode}</button>)}
          </div>
        )
      }
      const SeasonList = () => {
        const [selectedSeason, setSelectedSeason] = React.useState(2);
    
        return (
          <>
            <CoreTabs
              tab={selectedSeason - 1}
              id="season-list"
              onTabsToggle={event => setSelectedSeason(Number(event.target.getAttribute('tab')) + 1)}
            >
              <button
                type="button"
                id="season-tab-0"
                data-for="episode-list"
              >
                Season 1
              </button>
              <button
                type="button"
                id="season-tab-1"
                data-for="episode-list"
              >
                Season 2
              </button>
            </CoreTabs>
            <EpisodeList seasonNumber={selectedSeason} />
          </>
        )
      }
      ReactDOM.render(<SeasonList />, document.getElementById('react-single-panel'))
    </script>

    Installation

    Using NPM provides own element namespace and extensibility. Recommended:

    npm install @nrk/core-tabs  # Using NPM

    Using static registers the custom element with default name automatically:

    <script src="https://static.nrk.no/core-components/major/10/core-tabs/core-tabs.min.js"></script>  <!-- Using static -->

    Remember to polyfill custom elements if needed.

    Usage

    Attributes

    Name Optional Accepts values Description
    tab Yes number | string Used to set active tab. Number is index (starting at 0), string is an id-reference to a tab-element.

    HTML / JavaScript

    <core-tabs tab="{string | number}">       <!-- Optional. Used to set active tab String associates id-reference to tab element -->
      <button>Tab 1</button>                  <!-- Tab elements must be <a> or <button>. Do not use <li> -->
      <a href="#">Tab 2</a>
      <button>Tab 3</button>
      <button data-for="panel-2">Tab 4</button>    <!-- Point to a specific tabpanel -->
    </core-tabs>
    <div>Tabpanel 1 content</div>             <!-- First tabpanel is the next element sibling of core-tabs -->
    <div hidden>Tabpanel 2 content</div>      <!-- Second tabpanel. Use hidden attribute to prevent FOUC -->
    <div hidden id="panel-2">Tabpanel 3 content</div>      <!-- Third tabpanel. ID used to connect to tab 4 -->
    import CoreTabs from '@nrk/core-tabs'                 // Using NPM
    window.customElements.define('core-tabs', CoreTabs)   // Using NPM. Replace 'core-tabs' with 'my-tabs' to namespace
    
    const myTabs = document.querySelector('core-tabs')
    
    // Getters
    myTabs.tab        // Get active tab
    myTabs.tabs       // Get all tabs
    myTabs.panel      // Get active tabpanel
    myTabs.panels     // Get all tabpanels
    
    // Setters
    myTabs.tab = 0        // Set active tab from index
    myTabs.tab = 'my-tab' // Set active tab from id
    myTabs.tab = myTab    // Set active tab from element

    React / Preact

    import CoreTabs from '@nrk/core-tabs/jsx'
    
    <CoreTabs
      tab={String}                  // Optional. Sets current active tab by index or id
      ref={(comp) => {}}            // Optional. Get reference to React component
      forwardRef={(el) => {}}       // Optional. Get reference to underlying DOM custom element
      onTabsToggle={Function}       // Optional. Listen to toggle event
    >
      <button                  // Tab element must be <a> or <button>. Do not use <li>
        data-for={String}      // Id to element that contains the tab-related content
      >
        Tab 1
      </button>
      <a href="#">Tab 2</a>    
    </CoreTabs>
    <div>Tabpanel 1 content</div>           // First tabpanel is the next element sibling of CoreTabs
    <div hidden>Tabpanel 1 content</div>    // Second tabpanel. Use hidden attribute to prevent FOUC
    <div hidden>Tabpanel 1 content</div>    // Third tabpanel.  Use hidden attribute to prevent FOUC

    Events

    tabs.toggle

    Fired when toggling a tab:

    document.addEventListener('tabs.toggle', (event) => {
      event.target     // The tabs element
    })

    Styling

    All styling in documentation is example only. Both the tabs and tabpanels receive attributes reflecting the current toggle state:

    .my-tab {}                          /* Target tab in any state */
    .my-tab[aria-selected="true"] {}    /* Target only open tab */
    .my-tab[aria-selected="false"] {}   /* Target only closed tab */
    
    .my-tabpanel {}                     /* Target panel element in any state */
    .my-tabpanel:not([hidden]) {}       /* Target only open panel */
    .my-tabpanel[hidden] {}             /* Target only closed panel */

    FAQ

    Why aren't tabs wrapped in <ul><li>...</li></ul>?

    A <ul>/<li> structure would seem logical for tabs, but this causes some screen readers to incorrectly announce tabs as single (tab 1 of 1).

    Must panels always be next element siblings to <core-tabs>?

    The aria specification does not allow any elements that are focusable by a screen reader to be placed between tabs and panels. Therefore, core-tabs defaults to use the next element siblings as panels. This behaviour can be overridden, by setting up id on panel elements and the data-for attribute on tab element (for is deprecated). Use with caution and only do this if your project must use another DOM structure. Example:

    const myTabs = document.querySelector('core-tabs')
    myTabs.tabs.forEach((tabs, index) => tab.setAttribute('data-for', myTabs.panels[index].id = 'my-panel-' + index))

    How do I avoid panels flickering on initialization?

    When you know what panel will be visible on load, all others should have the hidden-attribute to avoid Flash of unstyled content (FOUC). If the active panel is unknown to your template, set hidden-attribute on all panels initially.

    Keywords

    none

    Install

    npm i @nrk/core-tabs

    DownloadsWeekly Downloads

    218

    Version

    2.2.1

    License

    MIT

    Unpacked Size

    217 kB

    Total Files

    12

    Last publish

    Collaborators

    • eschoien
    • balte
    • loftum
    • toshb
    • emte123
    • anderscan
    • lysebraate
    • opet
    • klizter
    • mikkelnygard
    • feiring
    • dervodev
    • grimbur
    • gardkroyer
    • kariaan
    • edplayz
    • elias-chairi
    • miatollaksvik
    • ytterbo
    • glennwedin
    • machineboycom
    • trulsl
    • syndrol
    • henit
    • mslhm
    • cbjerkan
    • hermangudesen
    • andreeldareide
    • henningkoller
    • oleob
    • meisen
    • espenhalstensen
    • hakonknutzen2
    • danjohnrk
    • olapeter
    • teodor-elstad
    • michaelpande
    • lorecaster
    • wwalmnes
    • nrk-ps-teamcity
    • vincent.andersson
    • swla
    • nrk-midas-jenkins
    • andorpandor
    • jarlelin
    • nrkrichard
    • gesi
    • gundelsby-nrk
    • jonstalecarlsen
    • nrk-sofie-ci
    • nytamin
    • jesperstarkar
    • skjalgepalg
    • eirikhalvard
    • astokke
    • n640071
    • n07073
    • henrik-mattsson
    • eirikbacker
    • haavardm
    • yr
    • nrk-kurator-jenkins
    • torgeilo
    • nrk-user-sync
    • dhdeploy
    • janerikbr
    • espenwa
    • rogerhmar
    • ovstetun
    • eivind
    • stianlj
    • haraldkj
    • mariusu
    • spesialsnorre
    • cristobal
    • knuthaug
    • thohalv
    • johnarne
    • eshaswini
    • morrow
    • oyvindeh
    • laat
    • toggu
    • nrk-jenkins
    • plomma
    • evjand
    • moltubakk
    • ingridguren
    • lu-lux
    • andersli
    • silje
    • stiandg
    • sjurlur
    • andipodnrk
    • pkej
    • yosrimti
    • morten.nyhaug
    • ingvildcath
    • erlend.jones
    • brneirik
    • mollerse
    • eriksalhus
    • frdrks
    • tbnrk
    • nordanke
    • mikaelrss
    • simonmitternacht
    • christiankarlsson9
    • martintorgersen
    • rebchr
    • steipal
    • discobus
    • ingvesund
    • martingundersen
    • tinkajts
    • hallvardlid
    • tomivar
    • ajaco
    • tobinus
    • mortenok
    • nrk-ark-deploy
    • jeangilbertlouis
    • heidimork
    • ingriddraagen
    • fridajalborg
    • bruusi
    • rosvoll
    • christianeide
    • enordby
    • artzag
    • glen_imrie
    • mia.aasbakken
    • elathamna
    • evjjan17
    • olatoft
    • kongsrud
    • chrpeter
    • ingvildforseth
    • esseb
    • talepre
    • haraldk76
    • stigok
    • johannesodland
    • anders993
    • vildefj
    • vildepk
    • andreakn
    • rolerboler
    • meloygutt
    • anders.baggethun