Navigation Tabs

Tabs

Progressive enhancement of a declarative tab group. The script wires ARIA roles, builds an animated sliding indicator, handles keyboard navigation, and optionally fetches panel content from a server on first activation. Three style variants — underline (default), pills, and boxed — combine with four placement options and two size presets. Individual tabs can be marked closable, and a manual activation mode lets users move focus with the keyboard without switching panels.

Quick Start

Add data-wf-tabs to a wrapper div containing a nav.wf-tab-nav with button.wf-tab elements and matching div.wf-tab-panel elements. Each button needs a data-wf-panel attribute whose value matches the id of the target panel. The first-enabled tab is activated automatically. Add the disabled attribute to a button to disable that tab.

General

Manage your account name, email address, and avatar.

Security

Change your password and manage two-factor authentication.

Notifications

Choose how and when you receive notifications.

Billing

Choose how and when you receive notifications.

HTML
<div class="wf-tab-group" data-wf-tabs>

  <nav class="wf-tab-nav" aria-label="Account settings">
    <button class="wf-tab" data-wf-panel="general">
      <i class="icon user" aria-hidden="true"></i> General
    </button>
    <button class="wf-tab" data-wf-panel="security">Security</button>
    <button class="wf-tab" data-wf-panel="notifications">Notifications</button>
    <!-- Disabled tab -->
    <button class="wf-tab" data-wf-panel="billing" disabled>Billing</button>
  </nav>

  <div id="general" class="wf-tab-panel">General content</div>
  <div id="security" class="wf-tab-panel">Security content</div>
  <div id="notifications" class="wf-tab-panel">Notifications content</div>
  <div id="billing" class="wf-tab-panel">Billing content</div>

</div>

Variants

Set data-wf-variant on the group container to change the visual style. The default (underline) uses an animated sliding indicator line below the active tab. pills renders tabs inside a pill/segmented-control tray. boxed renders browser-style bordered tabs where the active tab merges with the panel border.

Pills

Your personal files and folders.

Files shared with you by others.

Deleted files kept for 30 days.

Boxed

Edit the HTML structure of your page.

Write CSS rules and custom properties.

Add JavaScript interactions and logic.

HTML
<!-- Pills -->
<div class="wf-tab-group" data-wf-tabs data-wf-variant="pills">
  <nav class="wf-tab-nav" aria-label="File browser">
    <button class="wf-tab" data-wf-panel="files">Files</button>
    <button class="wf-tab" data-wf-panel="shared">Shared</button>
    <button class="wf-tab" data-wf-panel="archive">Archive</button>
  </nav>
  <div id="files"  class="wf-tab-panel">...</div>
  <div id="shared" class="wf-tab-panel">...</div>
  <div id="archive" class="wf-tab-panel">...</div>
</div>

<!-- Boxed -->
<div class="wf-tab-group" data-wf-tabs data-wf-variant="boxed">
  <nav class="wf-tab-nav" aria-label="Code editor">
    <button class="wf-tab" data-wf-panel="html">index.html</button>
    <button class="wf-tab" data-wf-panel="css">styles.css</button>
    <button class="wf-tab" data-wf-panel="js">app.js</button>
  </nav>
  <div id="html" class="wf-tab-panel">...</div>
  <div id="css"  class="wf-tab-panel">...</div>
  <div id="js"   class="wf-tab-panel">...</div>
</div>

Placement

Control where the tab nav appears relative to the panels with data-wf-placement . top (default) and bottom stack vertically. start and end place the nav in a vertical strip on the left or right and use a box-shadow active indicator instead of the sliding underline. On vertical placements the nav scrolls if the tab list exceeds the max height.

Start (left)

Project overview and summary.

Technical specifications and details.

Version history and changelog.

End (right)

Project overview and summary.

Technical specifications and details.

Version history and changelog.

Bottom

Nav appears below this panel when data-wf-placement="bottom" is set.

Tab 2 panel content.

Tab 3 panel content.

HTML
<!-- Start (left sidebar nav) -->
<div class="wf-tab-group" data-wf-tabs data-wf-placement="start">
  <nav class="wf-tab-nav" aria-label="...">...</nav>
  <div id="..." class="wf-tab-panel">...</div>
</div>

<!-- End (right sidebar nav) -->
<div class="wf-tab-group" data-wf-tabs data-wf-placement="end">
  ...
</div>

<!-- Bottom (nav below panels) -->
<div class="wf-tab-group" data-wf-tabs data-wf-placement="bottom">
  ...
</div>

Sizes

Use data-wf-size="sm" or data-wf-size="lg" on the group container. The default size needs no attribute. Sizes adjust tab padding and font size.

Small

Small tab panel.

Tab 2 content.

Tab 3 content.

Default (md)

Default tab panel.

Tab 2 content.

Tab 3 content.

Large

Large tab panel.

Tab 2 content.

Tab 3 content.

HTML
<div class="wf-tab-group" data-wf-tabs data-wf-size="sm">...</div>
<!-- default (no attribute needed): md -->
<div class="wf-tab-group" data-wf-tabs>...</div>
<div class="wf-tab-group" data-wf-tabs data-wf-size="lg">...</div>

Closable Tabs

Add data-wf-closable to any wf-tab button to make it closable. The script wraps the button in a div.wf-tab-wrapper[role=tab] , moves the label into a button.wf-tab-trigger , and appends a button.wf-tab-close × icon. Clicking the close button (or calling tabs.close(panelId) ) removes the tab and its panel from the DOM. If the closed tab was active, the next available tab is activated automatically.

HTML file content. Close tabs using the × button.

CSS file content.

JavaScript file content.

Markdown documentation.

HTML
<div class="wf-tab-group" data-wf-tabs>
  <nav class="wf-tab-nav" aria-label="Open files">
    <button class="wf-tab" data-wf-panel="html" data-wf-closable>index.html</button>
    <button class="wf-tab" data-wf-panel="css"  data-wf-closable>styles.css</button>
    <button class="wf-tab" data-wf-panel="js"   data-wf-closable>app.js</button>
  </nav>
  <div id="html" class="wf-tab-panel">...</div>
  <div id="css"  class="wf-tab-panel">...</div>
  <div id="js"   class="wf-tab-panel">...</div>
</div>

<!-- JS renders each tab as: -->
<div class="wf-tab-wrapper" role="tab" aria-selected="true" tabindex="0">
  <button class="wf-tab-trigger">index.html</button>
  <button class="wf-tab-close" aria-label="Close tab">
    <i class="icon cancel" aria-hidden="true"></i>
  </button>
</div>

AJAX Loading

Add data-wf-src="url" to a tab button to fetch its panel content from the server the first time the tab is activated. While loading, a spinner is shown inside the panel. Successfully loaded HTML is cached by URL, so later activations are instant. Use data-wf-no-cache on a tab or on the group container to disable caching and always re-fetch. The server receives X-Requested-With: XMLHttpRequest and X-WF-Tab: 1 request headers.

The "Remote content" and "No cache" tabs below will show a load error unless the paths /api/tabs/1 and /api/tabs/2 exist on your server. Create those files to see the full AJAX flow, including the loading spinner.

Static panel

This panel's content is defined in HTML – nothing is fetched from the server.

HTML
<div class="wf-tab-group" data-wf-tabs>

  <nav class="wf-tab-nav" aria-label="Remote content">
    <!-- Panel 1 is static; 2 load via AJAX; Panel 3 always re-fetches -->
    <button class="wf-tab" data-wf-panel="t1">Static panel</button>
    <button class="wf-tab" data-wf-panel="t2" data-wf-src="/api/tabs/2">Remote content</button>
    <button class="wf-tab" data-wf-panel="t3" data-wf-src="/api/tabs/3" data-wf-no-cache>No cache</button>
  </nav>

  <!-- Empty panels — JS fills them on first activation -->
  <div id="t1" class="wf-tab-panel"></div>
  <div id="t2" class="wf-tab-panel"></div>
  <div id="t3" class="wf-tab-panel"></div>

</div>

<!-- Disable caching for all tabs in the group -->
<div class="wf-tab-group" data-wf-tabs data-wf-no-cache>...</div>

Keyboard Navigation

The component follows the WAI-ARIA Tabs pattern . In the default auto activation mode, focusing a tab immediately selects it. Add data-wf-activation="manual" to the group to require an explicit Enter or Space keypress to activate the focused tab — useful for AJAX tabs where activation triggers a network request.

Key Action
Arrow Right / Arrow Left Move focus to the next/ previously enabled tab (horizontal placement).
Arrow Down / Arrow Up Move focus to the next/previous enabled tab (start or end placement).
Home Move focus to the first-enabled tab.
End Move focus to the last-enabled tab.
Enter / Space Activate the focused tab. Always works; required in manual activation mode.
HTML
<!-- Manual activation — focus moves freely; Enter/Space activates -->
<div class="wf-tab-group" data-wf-tabs data-wf-activation="manual">
  ...
</div>

Data Attributes

On .wf-tab-group

Attribute Values Description
data-wf-tabs Required. Marks the element for auto-init.
data-wf-placement top · bottom · start · end Where the tab nav appears relative to the panels. Default: top.
data-wf-variant pills · boxed Visual style variant. Omit for the default underline style.
data-wf-size sm · lg Tab size preset. Omit for the default medium size.
data-wf-active panel-id string Panel ID to activate on init instead of the first tab.
data-wf-activation manual Require Enter or Space to activate a focused tab (ARIA manual-activation mode).
data-wf-no-cache Disable AJAX response caching for all tabs in this group.

On .wf-tab

Attribute Description
data-wf-panel="id" Required. The id of the panel this tab controls.
data-wf-src="url" Fetch URL. Panel content is loaded from this URL the first time the tab is activated.
data-wf-no-cache Re-fetch the URL every time this tab is activated, ignoring any cached response.
data-wf-closable Wraps the tab in a closable structure with a separate close button.
disabled Native disabled attribute. Tab is unfocusable, unclickable, and skipped by keyboard nav.

CSS Properties

Property Default Description
--wf-tab-indicator-color var(--wf-primary) Colour of the animated underline indicator and the active tab text.
--wf-tab-track-color var(--wf-border) Color of the full-width track line beneath the tab nav.
--wf-tab-track-width 2px Thickness of the track line and the active indicator. Overridden to 1px by the boxed variant.

Programmatic API

Each initialized tab group exposes an instance on el._wfTabs .

Method Description
show(panelId) Activate a tab by its panel id. No-op if the tab is disabled.
next() Activate the next non-disabled tab. No-op if the last tab is active.
prev() Activate the previous non-disabled tab. No-op if the first tab is active.
close(panelId) Remove a closable tab and its panel from the DOM. If the tab was active, the next available tab is selected.
destroy() Remove all event listeners and disconnect the ResizeObserver.
JS
// Auto-init instance
const tabs = document.querySelector('[data-wf-tabs]')._wfTabs

tabs.show('security')
tabs.next()
tabs.prev()
tabs.close('css')
tabs.destroy()

// Manual init
import { WojoTabs } from '/assets/js/wf-production.min.js'

const instance = new WojoTabs(el, {
    placement:  'top',     // top | bottom | start | end
    variant:    'pills',   // default | pills | boxed
    size:       'sm',      // sm | md | lg
    activation: 'manual',  // auto | manual
    active:     'security',
    cache:      true,
}).init()

Events

All events are dispatched on the .wf-tab-group element.

Event Detail Description
wf:tab-show { panelId } Fired after a tab is activated and its panel is shown.
wf:tab-hide { panelId } Fired before a tab is deactivated (the outgoing tab).
wf:tab-close { panelId } Fired after a closable tab and its panel are removed from the DOM.
wf:tab-load { panelId, url } Fired after AJAX panel content is successfully fetched and injected.
wf:tab-load-error { panelId, url, error } Fired when an AJAX fetch fails. detail.error contains the Error object.
JS
const group = document.querySelector('[data-wf-tabs]')

group.addEventListener('wf:tab-show', e => {
  console.log('Activated panel:', e.detail.panelId)
  // sync browser URL hash
  history.replaceState(null, '', '#' + e.detail.panelId)
})

group.addEventListener('wf:tab-load', e => {
  console.log('AJAX loaded:', e.detail.url, 'into', e.detail.panelId)
})

group.addEventListener('wf:tab-close', e => {
  console.log('Closed tab:', e.detail.panelId)
})

i18n

The component uses WojoI18n for all user-facing strings. Register a locale before DOMContentLoaded to override the default English labels. Three strings are localizable: closeTabLabel (aria-label on the close button), loadingLabel (screen-reader text while AJAX loads), and loadError (displayed when a fetch fails).

JS
// Call before DOMContentLoaded / before auto-init fires
WojoI18n.setLocale('fr')
WojoI18n.register('fr', {
    tabs: {
        closeTabLabel: "Fermer l'onglet",
        loadingLabel:  'Chargement…',
        loadError:     'Erreur de chargement. Veuillez réessayer.',
    }
})

Class Reference

All modifier classes are available on multiple elements.

Class Description
wf-tab-group Root container. Add data-wf-tabs to activate. Accepts placement, variant, and size data attributes.
wf-tab-nav-wrapper JS-injected div wrapping the wf-tab-nav. Hosts the track pseudo-element and the indicator span.
wf-tab-nav The <nav> element containing tab buttons. The JS sets role="tablist". Provide an aria-label.
wf-tab A tab button. Requires data-wf-panel. JS adds role="tab", aria-selected, and tabindex.
wf-tab-indicator JS-injected animated sliding underline span. Hidden in pills, boxed, and vertical placements.
wf-tab-panel A content panel identified by id. Hidden via display:none; shown with JS-added [data-wf-active].
wf-tab-wrapper JS-injected wrapper div for closable tabs. Replaces the original .wf-tab button and receives role="tab".
wf-tab-trigger JS-injected inner button (the clickable label area) inside a closable tab wrapper.
wf-tab-close JS-injected close button (× icon) inside a closable tab wrapper. Click removes the tab and its panel.
wf-tab-panel-loader JS-injected loading state is shown inside a panel while AJAX content is being fetched.
wf-tab-panel-error JS-injected error message shown inside a panel when an AJAX fetch fails.
wf-tab-sr-only Visually hidden helper element for screen-reader-only text (e.g., loading label inside the loader).
active State class added by JS to the active .wf-tab or .wf-tab-wrapper.
[aria-selected=true] ARIA attribute set by JS on the active tab element.
[data-wf-active] Attribute added by JS to the visible panel (wf-tab-panel).
[hidden] Attribute added by JS to all inactive panels.
Show More