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 thetabs.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
<ul><li>...</li></ul>
?
Why aren't tabs wrapped in 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).
<core-tabs>
?
Must panels always be next element siblings to 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.