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

Readme

Keywords

none

Package Sidebar

Install

npm i @nrk/core-tabs

Weekly Downloads

428

Version

2.2.1

License

MIT

Unpacked Size

217 kB

Total Files

12

Last publish

Collaborators

  • torsrex
  • haraldsk
  • eskilgh
  • ragnaroh-nrk
  • daardal
  • arevjensen
  • julusian
  • madsern
  • andrefau
  • jacob-andersson
  • 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
  • janerikb
  • luminrk
  • supermeisen
  • vagifabilov
  • claudio-nrk
  • haakemon
  • zenangst
  • rannveignc
  • eschoien
  • balte
  • toshb
  • emte123
  • anderscan
  • opet
  • klizter
  • mikkelnygard
  • feiring
  • dervodev
  • grimbur
  • gardkroyer
  • kariaan
  • edplayz
  • elias-chairi
  • miatollaksvik
  • ytterbo
  • machineboycom
  • trulsl
  • mslhm
  • cbjerkan
  • hermangudesen
  • andreeldareide
  • henningkoller
  • oleob
  • 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