# Sliding Indicator Tabs

> Content tabs whose indicator slides and stretches between tabs while the panels change directionally and the box morphs between heights.

Canonical: https://gsapvault.com/ui-elements/sliding-indicator-tabs
Live demo: https://gsapvault.com/demos/sliding-indicator-tabs/index.html

| Property | Value |
|----------|-------|
| Type | ui-element |
| Tier | paid |
| Price | £5 |
| Difficulty | intermediate |
| Plugins | Core GSAP only |
| Techniques | tabs, keyboard-navigation, state-transition, hover-effect, micro-interaction |
| Uses Lenis | No |

## Overview

A tabbed panel where the indicator does the work. It slides and stretches between tabs, measured from each tab's own box rather than guessed from a fixed width, so labels of different lengths all get an indicator the exact width of the tab.

The panels then follow the indicator rather than cutting: the outgoing one leaves in the direction you are travelling, the incoming one arrives from the other side a frame later, and the panel box tweens between the two heights so nothing below the component jumps. Two looks ship from one attribute: an underline, and a filled segment at tab scale.

## Features

- Indicator slides and stretches between tabs, measured from each tab's own box so any label length lands exactly
- Directional panel change: the outgoing panel exits the way you are travelling, the incoming one arrives from the other side
- The panel box tweens between panel heights, so a shorter panel never snaps the page below it
- A soft hover pill follows the pointer across the tabs, gated to fine pointers so a tap cannot pin it open
- Two looks from one attribute: data-style="underline" and data-style="pill"
- Full WAI-ARIA tabs pattern with automatic activation: roving tabindex, arrow keys that wrap, Home and End
- Re-measures on resize, on fonts.ready and on a theme change, so an indicator is never left offset
- Overflowing tab lists scroll sideways with a faded edge, and the selected tab scrolls itself into view
- Optional data-tabs-hash deep linking that updates the URL without scrolling the page
- Emits a tabchange event carrying the new and previous index

## Use Cases

- Product, account and project detail pages where one record has several sections
- Settings and preference screens that would otherwise be one long scroll
- Dashboard cards that need to hold two or three views in the same footprint
- Documentation and pricing pages that switch between comparable blocks of content
- Any tab strip that has to survive a phone, where the labels no longer fit in a row

## Vibe-Code Ready Setup

This UI element includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

The indicator's position and width are measured from each tab's own box against the tab list, so labels of different lengths all get an indicator exactly their width, and a page zoom cannot throw the measurement off. The same pass records every panel's height, including the ones not on screen, so the box can ease between heights instead of jumping.

A change runs three movements on one beat: the indicator leads, the panel box eases to the new height, and the panels cross with a short slide whose direction follows the direction of travel. The outgoing panel is only hidden once it has fully left, so a transparent panel is never left covering the live one.

The hover pill is reserved for fine pointers, and under reduced motion every movement completes at once; the device conditions are complementary pairs, so a coarse-pointer device always matches something and the component always initialises.

## Documentation

How this UI element works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### What's Included

- `index.html`: the demo page and the markup to copy
- `assets/style.css`: component styles, plain CSS custom properties for colours
- `assets/script.js`: readable, commented source with an `onReady` guard

- The indicator is measured from each tab's own `offsetLeft` / `offsetWidth`, so a two-word label and a one-word label both get an indicator the exact width of the tab
- Panels change directionally: the outgoing panel leaves the way you are travelling, the incoming one arrives from the other side a frame later
- The panel box tweens between panel heights, so switching to a shorter panel never snaps the content below it
- A soft hover pill follows the pointer across the strip, gated to `hover: hover` pointers so a tap cannot pin it open
- Re-measures on `resize`, on `document.fonts.ready` and after a theme change, so an indicator is never left sitting off its tab
- An overflowing tab list scrolls sideways with a faded edge, and activating a tab scrolls it into view
- Roles: `tablist`, `tab` with `aria-selected` and `aria-controls`, `tabpanel` with `aria-labelledby`, and a roving `tabindex`

### Quick Start

**1. Add to your `<head>`:**

_Code snippet omitted: it ships with the download._

**2. Add before the closing `</body>` tag:**

_Code snippet omitted: it ships with the download._

**3. Copy the component markup from `index.html` into your page.**

The skeleton the script looks for:

_Code snippet omitted: it ships with the download._

What matters:

- `[data-tabs]` on the root, `[role="tablist"]` for the strip, `[role="tab"]` for each tab, and each tab's `aria-controls` pointing at its panel's `id`. Everything is found through those, so the class names can be renamed in the CSS.
- `.tabs__indicator` is optional in the sense that the tabs work without it, but it is the whole point. Give it no CSS `transform` and no CSS `left`: the script positions it with `x`, and an offset in both places doubles it.
- `.tabs__hover` is optional; delete it and the pointer highlight simply does not exist.
- `.tabs__strip` and `.tabs__scroller` are only needed if the list can overflow. Keep both if it might: the scroller does the scrolling and the strip carries the edge fades.
- The panels are absolutely stacked inside `.tabs__panels`, which is what lets the box tween between heights. Give exactly one panel no `hidden` attribute; the script hides the rest on load.

Options on the root:

| Attribute | Values | Default | What it does |
|-----------|--------|---------|--------------|
| `data-style` | `underline`, `pill` | `underline` | Underline bar, or a filled sliding segment at tab scale |
| `data-tabs-hash` | present / absent | absent | Syncs `location.hash` with each tab's `data-tab`, and reads it on load |
| `data-tab` (on a tab) | any slug | — | The tab's key, used for the hash and for `select()` |

Changing `data-style` at runtime changes the strip's padding, so follow it with a `resize` event or a `slidingTabs.refresh()` call and the indicator re-measures.

The variant switcher, the style switcher and the `.stage` wrapper in `index.html` are demo furniture, not part of the component; leave them behind.

### Keyboard & Accessibility

| Key | What it does |
|-----|--------------|
| `Tab` | Moves into the tab strip, landing on the selected tab only (roving tabindex), then on into the panel |
| `Arrow Right` / `Arrow Left` | Moves to the next or previous tab and selects it, wrapping at both ends |
| `Home` / `End` | Selects the first or last tab |
| `Enter` / `Space` | Selects the focused tab (native button behaviour) |

- Roles and state: `role="tablist"` with an `aria-label`, `role="tab"` with `aria-selected` and `aria-controls`, `role="tabpanel"` with `aria-labelledby`. Inactive panels carry the `hidden` attribute, so they are out of the accessibility tree and out of the tab order entirely; the active panel is focusable with `tabindex="0"` and anything focusable inside it is reachable as normal.
- Activation is **automatic**: an arrow key selects as it moves, which is the WAI-ARIA default for tabs whose panels are already in the page. Focus follows selection, so a screen reader announces the panel change.
- `prefers-reduced-motion: reduce`: the same end states with every duration at zero. The indicator, the panel and the box height land in one frame; nothing is disabled and nothing is hidden.
- Hover styling is gated behind `hover: hover`, so a tap on a phone cannot leave a tab looking hovered.
- The tab strip scrolls sideways only, so it carries **no** `data-lenis-prevent`: that attribute has no axis check and would hand the vertical wheel back to the browser for as long as the pointer sat over the strip.

### How It Works

**The indicator.** Each tab's position is read as `offsetLeft` / `offsetWidth` against the tab list, never `getBoundingClientRect()`, so a page zoom cannot come back multiplied and land the indicator at a multiple of its own offset. `gsap.to` then tweens the indicator's `x` and `width` to those numbers with `power3.out` over 340ms. Everything measured re-runs on `resize`, on `document.fonts.ready` (a webfont changes every tab width when it lands) and on the theme change the demo's toggle dispatches.

**The panel change.** The panels are absolutely stacked, so both can be on screen at once. A change tweens three things on one beat: the indicator leads, the box tweens to the new panel's height, and 80ms later the incoming panel slides 24px in from the side you came from while the outgoing one leaves the other way. The outgoing panel takes its `hidden` attribute back in the tween's `onComplete` and is reset to `x: 0, opacity: 1` there, so a transparent panel is never left covering the live one and the next measurement is not taken through a half-finished tween.

**The heights.** A hidden panel has no height to measure, so the measure pass removes the `hidden` attribute with `visibility: hidden` set, reads `offsetHeight`, and puts both back. Without that the box collapses to zero on the first change.

**Reduced motion and pointers.** `gsap.matchMedia` uses complementary condition pairs on both axes (`hover`/`pointer` and motion), so every device matches something: a set that only names fine pointers leaves a coarse-pointer phone matching nothing and the component never initialises at all.

### Customisation

- **Colours** live in `assets/style.css` as custom properties per theme. `--accent` is the indicator, the badge and the focus ring; `--accent-ink` is the text on it (keep the pair above 4.5:1).
- **Timings and easing** are the `DURATION` object at the top of `assets/script.js`. `SLIDE` is how far the panels travel, in pixels; much past 32 and the change stops reading as one move.
- **The indicator's shape** is `.tabs__indicator` in the underline block and in the `[data-style="pill"]` block. Height, radius and inset are all CSS; the script only supplies `x` and `width`.
- **Density**: tabs are 42px in the underline style and 34px in the pill style, on an 8px rhythm.

### Themes

Ships in two themes, `light` (the default) and `dark`: one design at two token values.

_Code snippet omitted: it ships with the download._

Nothing else changes: same markup, same script, and no theme name is ever read in JavaScript. The demo's toggle and the `?variant=dark` URL parameter only set that attribute.

To re-value it for your own brand, change the custom properties in the `body[data-variant="light"]` and `body[data-variant="dark"]` blocks at the top of `assets/style.css`: `--ground`, `--ground-2`, `--raised`, `--hover`, `--ink`, `--ink-2`, `--ink-3`, `--line`, `--line-strong`, `--accent`, `--accent-ink`, `--shadow` and the three radii. Every colour the component uses is one of those, so a rebrand never needs a selector override. Give text a solid colour rather than a translucent one, or a contrast checker cannot measure it.

### Requirements

- GSAP 3.12+ (core only, no plugins)
- No build step, no framework

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium UI element. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Sliding Indicator Tabs](https://gsapvault.com/ui-elements/sliding-indicator-tabs)
- [The Vault (£99 one-time, best value): everything in the catalogue, including future releases](https://gsapvault.com/effects)

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
