Something new is coming.Join the waitlist

Animated Tabs

PreviousNext

React tabs component with a shared Framer Motion layoutId indicator that morphs between active options. Lightweight, controllable, and drop-in for navs or filter chips.

Installation

npx shadcn@latest add https://tentui.com/r/animated-tabs.json

Usage

Pass an array of { value, label } objects. The component manages selection state internally — pass value and onValueChange if you want to control it yourself.

import { AnimatedTabs } from "@/components/animated-tabs";
 
const tabs = [
  { value: "overview", label: "Overview" },
  { value: "analytics", label: "Analytics" },
  { value: "reports", label: "Reports" }
];
 
export function Example() {
  return <AnimatedTabs tabs={tabs} defaultValue="overview" />;
}

Patterns

Controlled selection

Bind value to your own state when the active tab needs to drive other UI — a panel, a query string, an analytics event.

"use client";
 
import { useState } from "react";
import { AnimatedTabs } from "@/components/animated-tabs";
 
export function FilterRow() {
  const [tab, setTab] = useState("all");
 
  return (
    <AnimatedTabs
      tabs={[
        { value: "all", label: "All" },
        { value: "open", label: "Open" },
        { value: "closed", label: "Closed" }
      ]}
      value={tab}
      onValueChange={setTab}
    />
  );
}

Multiple groups on one page

The sliding indicator is driven by Framer Motion's layoutId. If you render two AnimatedTabs groups on the same page, give each one a unique layoutId — otherwise the indicator will animate between them across groups.

<AnimatedTabs tabs={primary} layoutId="primary-tabs" />
<AnimatedTabs tabs={secondary} layoutId="secondary-tabs" />

Props

PropTypeDefaultDescription
tabs{ value, label }[]Tabs to render. label accepts any ReactNode — strings, icons, fragments.
valuestringControlled active tab value. Pair with onValueChange.
defaultValuestringtabs[0].valueInitial active tab when uncontrolled.
onValueChange(value: string) => voidFires when the user picks a different tab.
layoutIdstring"animated-tabs-underline"motion layout id for the indicator. Override when multiple instances render on the same page.
classNamestringForwarded to the root <div>.

Accessibility

The root carries role="tablist" and each trigger is a real <button role="tab"> with aria-selected. Use Tab/Shift+Tab to move focus, and Enter or Space to activate. If you wire up associated panels, give each panel role="tabpanel" and link it to its trigger via aria-controls/id.