Demo

<div class="tpw-widget">...

Options

Note: if the viewport is too narrow, the Widget displays as an Accordion and changing its width won't switch "behavior"

TabPanel
Accordion

Semantic

POSH (Plain Old Semantic HTML)

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque tempus felis id urna vulputate maximus. Aliquam vitae arcu id nulla convallis aliquam. Vivamus at nisl semper, sagittis lectus eu, fringilla nisl.

This link is here to test keyboard navigation.

A11Y (AccessibilitY)

ARIA (Accessible Rich Internet Applications) FTW! (For The Win!)

Nulla a facilisis sapien. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Curabitur a est aliquet nisl consequat blandit. Aliquam consequat faucibus nisi eget molestie.

This link is here to test keyboard navigation.

Responsive

From TabPanel to Accordion

Sed vel euismod lectus, in viverra massa. Sed ullamcorper, elit ac aliquam sollicitudin, eros lacus volutpat ipsum, eget elementum sapien enim vel erat. Nunc vel ante ac purus accumsan dapibus. Ut vitae eros efficitur, auctor libero ut, porta lacus.

This link is here to test keyboard navigation.

Stress-Testing the Widget

Features

  • POSH

    POSH (Plain Old Semantic HTML)

    The Widget relies on Plain Old Semantic HTML (no jump-links needed!).
    Progressive Enhancement FTW (For The Win)!

  • A11Y

    Accessible

    First class support for screen-reader users!
    ARIA controls the rendering of their non-visual experience.

  • HTML

    Markup Agnostic

    Authors can use any heading they want to structure their content,
    they can even use a Definition List if they wish (dt / dd pairs).

  • Adaptive

    Adaptive

    The TabPanel becomes an Accordion if the tabs cannot "fit".
    Note that ARIA attributes will change accordingly.

  • Versatile

    Versatile

    Can work as an Accordion out-of-the-box.
    Accordion's icons can either be displayed to the right or left of the text.

  • RTL/LTR

    RTL Friendly

    Tabs flow according to script direction (ltr, rtl).
    Icon's positioning will obey script direction too.

  • Keyboard

    Keyboard Friendly

    Supports keyboard navigation (see below).
    Users can skip the entire Widget or reach the first tab/header.

Assuming the focus is on the Widget

For TabPanel

tab

When focus moves to the tabs, places focus on the active "tab" element.
When the tabs contain the focus, moves focus to the next element in the page tab sequence outside the tabs, which is typically either the first focusable element inside the tab panel or the tab panel itself.

Left Arrow

Moves focus to the previous tab. If focus is on the first tab, moves focus to the last tab and activates the newly focused tab.

Right Arrow

Moves focus to the next tab. If focus is on the last tab element, moves focus to the first tab and activates the newly focused tab.

home

Moves focus to the first tab and activates the newly focused tab.

end

Moves focus to the last tab and activates the newly focused tab.

esc

Toggle a skip link that offers to either bypass the Widget or to bring focus back on the first header.

For Accordion

enter space

When focus is on the Accordion header for a collapsed panel, expands the panel.

When focus is on the Accordion header for an expanded panel, collapses the panel.

tab

Moves focus to the next focusable element; all focusable elements in the Accordion are included in the page tab sequence.

shift + tab

Moves focus to the previous focusable element; all focusable elements in the Accordion are included in the page tab sequence.

Down Arrow

If focus is on an Accordion header, moves focus to the next Accordion header.

If focus is on the last Accordion header, either does nothing or moves focus to the first Accordion header.

Up Arrow

If focus is on an Accordion header, moves focus to the previous Accordion header.

If focus is on the first Accordion header, either does nothing or moves focus to the last Accordion header.

home

When focus is on an Accordion header, moves focus to the first Accordion header.

end

When focus is on an Accordion header, moves focus to the last Accordion header.

esc

Toggle a skip link that offers to either bypass the Widget or to bring focus back on the first header.

Browser Support

This library relies on ResizeObserver but we have a polyfill for browsers that don't support it—as the table below shows.

Browser Support
Sans PolyfillWith Polyfill
WinOSXAndroidiOSWinOSXAndroidiOS
Chrome6464858631316847
Edge80808080
Firefox6969794242657.2
Internet Explorerx10
Opera52521717
Safarix13.113.16.26.27
Samsung9.24.5
UC Browserx12.110.4
Yandexxx14.1214.12

Notes:

  • The polyfill works in Safari 5.1, but in that browser the resizing of the widget is only triggered via window.resize.
  • Accordion icons do not show in IE10. That would require to "dumb down" the styling, going with encodedURI instead of "plain" SVG.

Le Code

Try it on CodePen First!

There is no wrapper requirement. Anything may go in between your headings. The script wraps that content inside <div>s that become the "panels".

Install

"Old School"

Download the latest release of tabpanelwidget-x.x.x.zip which includes:

  • The minified Script
  • The minified Polyfill
  • A stylesheet (.scss) that contains "variables"
  • A minified stylesheet that is the output of the above file
Setup

Wrap your headings and their relevant content inside a div (or else) to which you apply the class tpw-widget.

Note: If you are using a Definition List, then simply apply that class to the dl itself.
<!-- .tpw-widget -->
<!-- This wrapper can be anything (article/div/dl/whatever) -->
<div class="tpw-widget">
    <!--
    You CAN use any (and as many) headings (h2/h3/h4/h5/h6)
    You MUST use the SAME heading level throughout a Widget
    You CAN use a <dl> but it has to be made of dt/dd pairs
    "tpw-selected" dictates which panel(s) should be opened
    -->
    <h3 class="tpw-selected">Lorem</h3>
    <!--
    Whatever you put in between the headings is wrapped by
    the script inside <div>s to become the panels' content
    -->
    <p>...</p>
    <ul>...</ul>
    <blockquote>...</blockquote>
    <h3>Ipsum</h3>
    ...
    <h3>Dolor</h3>
    ...
    <h3>Sit Amet</h3>
    ...
</div>
<!-- /.tpw-widget -->

Include the stylesheet in the head of your document:

<link href="/PATH_TO_FILE/tabpanelwidget.min.css" rel="stylesheet" />

Include this before </body>:

<script src="/PATH_TO_FILE/tabpanelwidget.min.js"></script>
<script>
  // remove this if you do not want to serve the polyfill
  if (!window.ResizeObserver) {
    const script = document.createElement("script")
    script.src = "/PATH_TO_FILE/tabpanelwidget-polyfill.min.js"
    document.head.appendChild(src)
  }
  // Instantiate TabPanelWidget
  Tabpanelwidget.autoinstall();
</script>

Or you may choose to load all files from CDN:

<link href="//cdn.jsdelivr.net/npm/tabpanelwidget@1.2.0/dist/tabpanelwidget.min.css" rel="stylesheet" />
<script src="//cdn.jsdelivr.net/npm/tabpanelwidget@1.2.0/dist/tabpanelwidget.min.js"></script>
<script>
  // remove this if you do not want to serve the polyfill
  if (!window.ResizeObserver) {
    const script = document.createElement("script")
    script.src = "//cdn.jsdelivr.net/npm/tabpanelwidget@1.2.0/dist/tabpanelwidget-polyfill.min.js"
    document.head.appendChild(src)
  }
  // Instantiate TabPanelWidget
  Tabpanelwidget.autoinstall();
</script>

The above will attach the script to all containers with the class tpw-widget.


Usage
Classes

"Out-of-the-box", the Widget will appear as shown in the Demo section but a few classes give you different options.

All classes are meant to be applied to the Widget (the wrapper) with the exception of tpw-selected which, applied to a "heading" (or multiple headings in the case of an Accordion), will let you arbitrarily choose which panel(s) to open by default (the one(s) associated with that/those heading(s)).

"Variables"

tabpanelwidget.min.css is the output of tabpanelwidget.scss. The latter contains variables that will let you customize the Widget's tabs, headers, and panels (their color, background, border, border-radius, padding, margin, etc.).

The comments in the SCSS file contain a great deal of information; make sure to check them out!.

"Vanilla"

import * as Tabpanelwidget from "tabpanelwidget"
import "tabpanelwidget/dist/tabpanelwidget.min.css"

// find all .tpw-widget in page and install them
Tabpanelwidget.autoinstall()

// or specify element to install (and uninstall)
const el = document.querySelector('#my-element')
// keep in mind install completes asynchronously so uninstall is returned by promise
const uninstall = await Tabpanelwidget.install(el)
// or worse, can use a callback as second arg (make sure you don't call uninstall before it's set in this case):
// let uninstall; Tabpanelwidget.install(el, _uninstall => (uninstall = _uninstall))
// later...
if (uninstall) uninstall()

VueJS"Vue.js"

<script>
import VueTabpanelwidget from "tabpanelwidget/vue"
import "tabpanelwidget/dist/tabpanelwidget.min.css"

Vue.use(VueTabpanelwidget)
// or Vue.component("Tabpanelwidget", VueTabpanelwidget)
// or in component, components: { VueTabpanelwidget, ... }
<Tabpanelwidget :heading="2" :mode="accordion" :selected-idxs="[1]" :tabs="['a', 'b', 'c']" rtl animate skin="pills" icon-style="fancy" centered disconnected icons-at-the-end rounded>
  <template v-slot:panel-0="">
    override content for panel 0
  </template>
</Tabpanelwidget>

ReactReact

import ReactTabpanelwidget from "tabpanelwidget/react"
import "tabpanelwidget/dist/tabpanelwidget.min.css"

<ReactTabpanelwidget heading={2} mode={'accordion'} selected-idxs={[1]} rtl animate skin={'pills'} icon-style={'fancy'} centered disconnected icons-at-the-end rounded>
  <ReactTabpanelwidget.Heading>heading 1</ReactTabpanelwidget.Heading>
  <ReactTabpanelwidget.Panel>panel 1</ReactTabpanelwidget.Panel>
</ReactTabpanelwidget>

AngularAngular

Coming soon! Even sooner if we receive a PR ;)

FAQ

How come the Headers appear broken in my Widget?

Headers in Accordions are styled with display:flex so if you have text nodes and "tags" (i.e. <code>) as direct children of the "headers" then things may appear very misaligned, whitespace may be missing, etc.

To easily fix this, you can simply wrap the content of your headers inside a <span>.

How come the Tabs appear broken in my Widget?

If your tabs look broken, it is because there are some CSS rules in your document that are styling the headings in the Widget.

The TabPanelWidget stylesheet tries to prevent such styling by increasing selector specificity and "resetting" common declarations (font-size and margins for example), but things will look broken if other styles target—and overwrite— the styles of the headings used as Tabs/Headers.
See below how you may fix such issues.

How come my Widget appears broken in IE10?

IE 10 does not "support" the hidden attribute so if you want to support IE10 and does not include a stylesheet like Normalize, you will need to add the following to your stylesheet:

[hidden] {display: none;}
How can I fix issues due to global styles?

An easy way to prevent your CSS rules from styling the Widget's headings is to rely on the :not() pseudo-class. You'd need to attach :not(.tpw-hx) to the selector in which those headings are the target.
For example, a selector like this for your headings:

section h3 {margin: 1rem 0;}

Would become this:

section h3:not(.tpw-hx) {margin: 1rem 0;}

The above will style your level 3 headings inside section without styling the h3 used as the Widget's tabs/headers—even if the Widget is inside a section.

Please keep in mind that using :not() will bump the specificity of your selector/rule.

Can we style widgets differently on the same page?

Yes, each Widget can be "skinned" individually via various classes. (See the usage section above or the Wiki).

Note that—to improve performance—whatever styles you are not using with your Widget(s) should be deleted from the stylesheet.

How can I remove the focus ring from the panels?
The focus ring is meant to help keyboard users navigate through the Widget. If you need to remove that style, you can do one of the following:
  • :focus .tpw-panel {
      box-shadow: none !important;
      outline: none !important;
    }
  • :focus:not(:focus-visible),
    :focus:not(:focus-visible) .tpw-panel {
      box-shadow: none !important;
      outline: none !important;
    }

The former will remove the focus ring for all users, the latter will only remove the focus ring for mouse users (as long as the browser supports :focus-visible).

How to prevent reflow below the TabPanel?

Depending on the content of the panels in the Widget you can—to some extent—minimize the reflow (below the Widget) by styling the Widget with a min-height, as we do with the TabPanel in the Demo section.

How come I cannot customize the Widget?

You may be trying to style the tabs and/or headers via rules that do not carry enough specificity.

To give a decent "skin" to the Widget "out-of-the-box", we tried to find a balance between unstyling elements that would inherit styles from a document's stylesheets and also make sure to use selectors that would carry enough "weight" (i.e. the specificity of many of our rules is greater than 0,1,1,0 ).

For minor customization, we suggest you use the scss file and edit its variables. For more serious changes, we suggest you edit the rules or declarations directly in the tabpanelwidget.scss stylesheet rather than trying to overwrite styles by writing new rules.

How come I cannot style the dt/dd in the Widget?

<dl>, <dt> and <dd> as main elements of a Widget are transformed into <div>. The reason for this is due to HTML structure. TabPanels need a tablist and it would be malformed HTML to insert an empty <div> as first-child of the Definition List.

Nonetheless, you can style the tabs, headers, and tabpanels using these classes:

  • .tpw-tab
  • .tpw-header
  • .tpw-panel
How come the Widget does not show on the page?

If the Widget appears to be styled with visibility:hidden, it is certainly because your markup is malformed (i.e. if you have a <p> as direct child of a <dl>).

To prevent this from happening, you can run the markup of your Widget through the validator.

How can I style the Widget when it is "inactive"?

You can use the selector in the following example to style a Widget (or its children) when it is not displayed as a TabPanel nor an Accordion (when the script has not transformed them):

.tpw-widget:not(.tpw-js) {margin: 0;}
How can I minimize a FOUC (Flash Of Unstyled Content)?

Obviously, there are a lot of changes in the markup and the styling once the script mounts the Widget(s), but it is possible to reduce the FOUC effect—in modern browsers—by including the following rule in your stylesheet:

html:not(.no-js):not(.tpw-\!fouc) .tpw-widget {
  visibility: hidden;
}

The class tpw-!fouc is set by the script while the class no-js is used as a generic hook to style elements according to script support (if you use a different class to achieve this, then use it in the selector above in-lieu of no-js).

Note that for the first Widget on this page, we also rely on a min-height to minimize the reflow.

How can I change the styling of the tabs, headers, etc.?

You can either edit the value of the variables in the SCSS file (i.e. $fontSize:1.5rem) or write new rules. If you choose to do the latter, you can rely on these selectors:

  • .tpw-hx to target the headings (mostly to reset their styling)
  • .tpw-tab to target the Tabs in the TabPanels
  • .tpw-header to target the Headers in the Accordions

To style the active tab or header, use these selectors:

  • .tpw-selected .tpw-tab
  • .tpw-selected .tpw-header

To target an active panel (or multiple open panels in case of an Accordion), you can use the following:

  • .tpw-selected + div > .tpw-panel

Note that overriding the styling of headings will require more specificity than overriding the styling of Tabs/Headers via .tpw-tab and .tpw-header. This is because the styling of .tpw-hx is meant to overwrite styles that could "leak" onto the headings used in the Widget.

Can the Widget interfere with my page's stacking contexts?

The Widget is positioned and styled with z-index:0 which means the Widget and its children will not show over any other positioned elements in your page that have an explicit z-index value greater than 0.

How can I "mount" a specific Widget on my page?
To target a specific Widget, you can do the following:
<script>
    // TabPanelWidget
    const uninstall = await Tabpanelwidget.install(document.querySelector('#ThisWidgetOnly'));
    // or worse, can use a callback as second arg (make sure you don't call uninstall before it's set in this case):
    // let uninstall; Tabpanelwidget.install(el, _uninstall => (uninstall = _uninstall))
    // ... later, if needed
    uninstall();
</script>
It's working! What should I do next?

There is a good chance that your Widget uses only a subset of the stylesheet (which includes many different styles for TabPanels and Accordions). So you should trim the stylesheet to keep only the styles you are actually using—to load a much smaller file.

Don't see your question? Ask on GitHub!