fluid-tabs

A lightweight, zero-dependency tabs library with a smooth animated indicator, drag/wheel/swipe scrolling, and multiple visual styles.

Just add the tab-bar class to a row of buttons.

Features

  • No dependencies
  • Three styles out of the box: underline, classic tabs, and a sliding segmented control
  • Animated indicator that slides between tabs with an elastic stretch, plus a spring tease on hover
  • Content panels cross-fade and the container resizes between tabs
  • Too many tabs? They scroll horizontally — drag, wheel, or swipe, with momentum and edge fades
  • Opt into wrapping onto multiple rows instead of scrolling
  • Swipe left/right on the content to change tabs on touch
  • Interruptible: click another tab mid-animation and the indicator redirects, no waiting
  • Detached tabs: the bar and its panels don't have to be siblings
  • Fires a tab-changed event, and works with a plain .click()
  • Themeable with a handful of CSS variables

Basic Usage

A .tab-bar of .tab-bar-buttons, with a matching .tab-contents next to it. Buttons and panels pair up by data-tab; mark the starting pair with active:

The overview panel. Click a tab and the indicator slides across while the panel cross-fades and the container resizes to fit.
The details panel — a little taller, so you can see the height animate.

Line two.
Line three.
The reviews panel.
<div class="tab-bar">
  <button class="tab-bar-button active" data-tab="overview">Overview</button>
  <button class="tab-bar-button" data-tab="details">Details</button>
  <button class="tab-bar-button" data-tab="reviews">Reviews</button>
</div>
<div class="tab-contents">
  <div class="tab-content active" data-tab="overview">…</div>
  <div class="tab-content" data-tab="details">…</div>
  <div class="tab-content" data-tab="reviews">…</div>
</div>

Styles

Add a tab-style-* class to the bar and its panels. Without one you get the underline style. Classic tabs connect into a filled panel; slide is a pill-shaped segmented control where the spring/tease is most visible.

Profile panel — the active tab joins onto this bordered panel.
Settings panel.
Billing panel.
Day view. Hover an adjacent tab to feel the spring tease.
Week view.
Month view.
Year view.
<div class="tab-bar tab-style-tabs">…</div>
<div class="tab-contents tab-style-tabs">…</div>

<div class="tab-bar tab-style-slide">…</div>
<div class="tab-contents tab-style-slide">…</div>

Detached panels

The bar and its panels don't have to be siblings. Give the bar an id and point the contents at it with data-tab-bar:

— anything can sit between the bar and its panels —

Alpha panel, linked by id.
Beta panel.
Gamma panel.
<div class="tab-bar" id="account">…</div>
<p>…anything in between…</p>
<div class="tab-contents" data-tab-bar="account">…</div>

Scrolling & wrapping

When there are more tabs than fit, the bar scrolls horizontally by default — drag it, wheel over it, or swipe the content. A soft fade marks whichever edge has hidden tabs, and selecting a tab scrolls it into view.

Overview panel.
Specs panel.
Compatibility panel.
Pricing panel.
FAQ panel.
Support panel.

Prefer they wrap onto multiple rows instead? Add data-tab-wrap. The indicator follows the active tab across rows.

Overview panel.
Specs panel.
Compatibility panel.
Pricing panel.
FAQ panel.
Support panel.
<!-- scrolls (default) -->
<div class="tab-bar">…</div>

<!-- wraps to multiple rows -->
<div class="tab-bar" data-tab-wrap>…</div>

Disable swipe

On touch, swiping the content steps between tabs. Add data-tab-no-swipe to turn that off — handy when a panel holds its own swipeable carousel. The buttons still work.

Swipe this on a touch device and nothing happens — use the buttons.
Second panel.
Third panel.
<div class="tab-bar" data-tab-no-swipe>…</div>

Events

The bar dispatches a tab-changed event whenever the active tab changes. event.detail is the new tab's data-tab:

Red panel.
Green panel.
Blue panel.

tab-changed: (waiting)

bar.addEventListener("tab-changed", e => {
  console.log("switched to", e.detail)
})

Programmatic & dynamic

Switch tabs by calling .click() on a button — the library listens for normal clicks. Bars added after load are picked up with FluidTabs.init() (idempotent; pass a root element to scan just a subtree):

Left panel.
Middle panel.
Right panel.
// jump to a tab
bar.querySelector('[data-tab="reviews"]').click()

// initialise bars added after load
FluidTabs.init()          // whole document
FluidTabs.init(sectionEl) // within an element

Theming

Tweak the feel with a handful of CSS variables, globally or per-bar: --tab-transition-duration, --tab-transition-easing, --tab-slide-lag (the elastic stretch), --tab-tease-x (hover tease), and --tab-fade (scroll edge fade). This bar is slowed down with an exaggerated spring:

Alpha panel.
Bravo panel.
Charlie panel.
Delta panel.
.tab-bar {
  --tab-transition-duration: .55s;
  --tab-tease-x: 24px;
  --tab-slide-lag: .6;
}