This package has been deprecated

Author message:

NRK is no longer actively maintaining and developing Core-components

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

Dependencies (0)

    Dev Dependencies (0)

      Package Sidebar

      Install

      npm i @nrk/core-tabs

      Weekly Downloads

      364

      Version

      2.2.1

      License

      MIT

      Unpacked Size

      217 kB

      Total Files

      12

      Last publish

      Collaborators

      • kristjgr
      • henrk
      • sokkemannen
      • ihlnrk
      • knutbo
      • hammeralf
      • janerikbr
      • thormodb
      • siivers
      • torsrex
      • haraldsk
      • eskilgh
      • ragnaroh-nrk
      • daardal
      • arevjensen
      • julusian
      • madsern
      • andrefau
      • jfjeldskaar
      • muddah
      • jensrage
      • oysteinkoppang
      • phajsi
      • jorn_georg
      • bjornhels
      • halvorh
      • morten-nrk
      • nicklassvendsrud
      • kjellvnnrk
      • sanderknrk
      • nikolaia
      • eirikjstnrk
      • carinafraning
      • helenper
      • stefanogdennrk
      • jimmeloysund
      • tobiasrp
      • martiosk
      • jimalexberger
      • gunderwonder
      • hamnis
      • luminrk
      • supermeisen
      • vagifabilov
      • claudio-nrk
      • haakemon
      • zenangst
      • rannveignc
      • eschoien
      • balte
      • toshb
      • emte123
      • opet
      • klizter
      • mikkelnygard
      • feiring
      • dervodev
      • grimbur
      • gardkroyer
      • kariaan
      • edplayz
      • elias-chairi
      • miatollaksvik
      • ytterbo
      • machineboycom
      • trulsl
      • mslhm
      • cbjerkan
      • hermangudesen
      • andreeldareide
      • henningkoller
      • espenhalstensen
      • danjohnrk
      • olapeter
      • teodor-elstad
      • lorecaster
      • nrk-ps-teamcity
      • swla
      • nrk-midas-jenkins
      • andorpandor
      • nrkrichard
      • gesi
      • gundelsby-nrk
      • jonstalecarlsen
      • nrk-sofie-ci
      • nytamin
      • jesperstarkar
      • skjalgepalg
      • eirikhalvard
      • astokke
      • n640071
      • n07073
      • henrik-mattsson
      • haavardm
      • yr
      • nrk-kurator-jenkins
      • torgeilo
      • nrk-user-sync
      • dhdeploy
      • espenwa
      • ovstetun
      • stianlj
      • haraldkj
      • mariusu
      • 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
      • tbnrk
      • nordanke
      • simonmitternacht
      • martintorgersen
      • rebchr
      • steipal
      • discobus
      • martingundersen
      • tinkajts
      • hallvardlid
      • tomivar
      • ajaco
      • tobinus
      • mortenok
      • nrk-ark-deploy
      • jeangilbertlouis
      • heidimork
      • ingriddraagen
      • fridajalborg
      • bruusi
      • rosvoll
      • christianeide
      • enordby
      • glen_imrie
      • mia.aasbakken
      • elathamna
      • evjjan17
      • olatoft
      • kongsrud
      • chrpeter
      • ingvildforseth
      • haraldk76
      • stigok
      • johannesodland
      • anders993
      • vildefj
      • vildepk
      • rolerboler
      • meloygutt
      • anders.baggethun