Skip to content

Animated Tabs Indicator

A tab row where the active-state highlight slides between tabs instead of popping.

Freeminimalplayful
Category
Components
Added
2026-08-22
Deps
1
Updated
2026-09-01
Preview

Props

Requires framer-motion
"use client";

import { useState } from "react";
import { motion } from "framer-motion";

export function AnimatedTabs({
  tabs = ["Overview", "Pricing", "Reviews"],
  shape = "pill",
}: {
  tabs?: string[];
  shape?: "pill" | "rounded";
}) {
  const [active, setActive] = useState(0);

  return (
    <div className="inline-flex gap-1 rounded-full border border-neutral-200 bg-neutral-100 p-1">
      {tabs.map((tab, i) => (
        <button
          key={tab}
          onClick={() => setActive(i)}
          className={`relative px-4 py-1.5 text-sm font-semibold transition-colors ${
            active === i ? "text-white" : "text-neutral-500 hover:text-neutral-800"
          }`}
        >
          {active === i && (
            <motion.span
              layoutId="tab-indicator"
              className={`absolute inset-0 bg-red-500 ${shape === "pill" ? "rounded-full" : "rounded-md"}`}
              transition={{ type: "spring", stiffness: 400, damping: 32 }}
            />
          )}
          <span className="relative">{tab}</span>
        </button>
      ))}
    </div>
  );
}
Notes

About this component

Building a sliding tab indicator by hand usually means measuring each tab's offsetLeft and width on every render and animating a separate absolutely-positioned bar to match, which is brittle the moment tabs resize or wrap. This component skips that math: the highlight pill renders as a child inside whichever tab button is currently active, given the same layoutId on every render, and Framer Motion's shared layout projection notices it reappearing in a new DOM parent and FLIP-animates it there automatically, tuned with a snappy spring at stiffness 400 and damping 32. Reach for this over a plain CSS underline-on-hover when the indicator actually needs to travel between states; a static underline via border-bottom is lighter weight and fine for simple nav. One thing worth knowing before copying this: the indicator element has to render before the label text in the DOM, or get an explicit z-index, or the solid pill sits on top of the label instead of behind it. The shipped version relies on the label's own relative positioning to win that stacking order, which is easy to accidentally undo while restyling.

Curator’s note

One shared layoutId, conditionally rendered per active tab — that's the entire mechanism. No offset math, no refs, no measuring; Framer's shared layout projection does the FLIP because it recognizes the same layoutId reappearing in a new parent between renders.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Featured
NewPro

A nav underline that stretches with the distance it travels and settles back to width.

navunderlinespring

A macOS-style dock where icons magnify based on real distance from the cursor.

docknavmagnetic

An iOS-style pill switch whose highlight slides between options.

segmentedcontrolswitch