Animated Tabs
Tabs with physics-aware transitions and sticky indicators.
01
Flip Tabs
Project Overview Content
Source Code
"use client";
import React, { useRef, useEffect, useState } from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
import gsap from "gsap";
export function FlipTabs({
tabs,
defaultValue,
className
}: {
tabs: { label: string; value: string; content: React.ReactNode }[];
defaultValue?: string;
className?: string;
}) {
const [activeTab, setActiveTab] = useState(defaultValue || tabs[0].value);
const containerRef = useRef<HTMLDivElement>(null);
return (
<TabsPrimitive.Root
value={activeTab}
onValueChange={setActiveTab}
className={cn("w-full space-y-8", className)}
>
<TabsPrimitive.List className="flex items-center gap-2 p-1.5 bg-zinc-950 rounded-2xl border border-white/5 w-fit">
{tabs.map((tab) => (
<TabsPrimitive.Trigger
key={tab.value}
value={tab.value}
className={cn(
"px-6 py-2.5 text-sm font-bold transition-all duration-300 outline-none rounded-xl relative",
activeTab === tab.value
? "bg-zinc-800 text-white shadow-xl translate-y-[-2px]"
: "text-zinc-500 hover:text-zinc-300"
)}
>
{tab.label}
</TabsPrimitive.Trigger>
))}
</TabsPrimitive.List>
<div className="perspective-[1000px]">
{tabs.map((tab) => (
<TabsPrimitive.Content
key={tab.value}
value={tab.value}
className="outline-none data-[state=inactive]:hidden"
>
<FlipContent>{tab.content}</FlipContent>
</TabsPrimitive.Content>
))}
</div>
</TabsPrimitive.Root>
);
}
function FlipContent({ children }: { children: React.ReactNode }) {
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (contentRef.current) {
gsap.fromTo(contentRef.current,
{ rotateX: -15, opacity: 0, scale: 0.95, y: 20 },
{ rotateX: 0, opacity: 1, scale: 1, y: 0, duration: 0.6, ease: "back.out(1.7)" }
);
}
}, []);
return (
<div
ref={contentRef}
className="bg-zinc-900 border border-white/5 rounded-[2.5rem] p-10 min-h-[250px] shadow-2xl origin-top"
>
{children}
</div>
);
}02
Fluid Indicator
Project Overview Content
Source Code
"use client";
import React, { useRef, useEffect, useState } from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
import gsap from "gsap";
export function FluidTabs({
tabs,
defaultValue,
className
}: {
tabs: { label: string; value: string; content: React.ReactNode }[];
defaultValue?: string;
className?: string;
}) {
const [activeTab, setActiveTab] = useState(defaultValue || tabs[0].value);
const triggerRefs = useRef<{ [key: string]: HTMLButtonElement | null }>({});
const indicatorRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const activeTrigger = triggerRefs.current[activeTab];
if (activeTrigger && indicatorRef.current) {
const { offsetLeft, offsetWidth } = activeTrigger;
gsap.to(indicatorRef.current, {
x: offsetLeft,
width: offsetWidth,
duration: 0.5,
ease: "elastic.out(1, 0.8)",
});
}
}, [activeTab]);
return (
<TabsPrimitive.Root
value={activeTab}
onValueChange={setActiveTab}
className={cn("w-full space-y-8", className)}
>
<div className="relative inline-flex p-1 bg-secondary-1/5 rounded-2xl border border-white/5" ref={containerRef}>
<div
ref={indicatorRef}
className="absolute h-[calc(100%-8px)] top-1 left-0 bg-primary-1 rounded-xl shadow-[0_0_20px_rgba(var(--primary-1-rgb),0.3)] z-0"
/>
<TabsPrimitive.List className="relative z-10 flex items-center">
{tabs.map((tab) => (
<TabsPrimitive.Trigger
key={tab.value}
value={tab.value}
ref={(el) => { triggerRefs.current[tab.value] = el; }}
className={cn(
"px-6 py-2.5 text-sm font-semibold transition-colors duration-300 outline-none rounded-xl",
activeTab === tab.value ? "text-white" : "text-muted-foreground hover:text-white"
)}
>
{tab.label}
</TabsPrimitive.Trigger>
))}
</TabsPrimitive.List>
</div>
<div className="relative overflow-hidden min-h-[200px] rounded-3xl bg-secondary-1/5 border border-white/5 p-8">
{tabs.map((tab) => (
<TabsPrimitive.Content
key={tab.value}
value={tab.value}
className="outline-none data-[state=inactive]:hidden"
>
<FluidContent>{tab.content}</FluidContent>
</TabsPrimitive.Content>
))}
</div>
</TabsPrimitive.Root>
);
}
function FluidContent({ children }: { children: React.ReactNode }) {
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (contentRef.current) {
gsap.fromTo(contentRef.current,
{ opacity: 0, y: 10, filter: "blur(4px)" },
{ opacity: 1, y: 0, filter: "blur(0px)", duration: 0.4, ease: "power2.out" }
);
}
}, []);
return <div ref={contentRef}>{children}</div>;
}03
Gooey Tabs
Project Overview Content
Source Code
"use client";
import React, { useRef, useEffect, useState } from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
import gsap from "gsap";
export function GooeyTabs({
tabs,
defaultValue,
className
}: {
tabs: { label: string; value: string; content: React.ReactNode }[];
defaultValue?: string;
className?: string;
}) {
const [activeTab, setActiveTab] = useState(defaultValue || tabs[0].value);
const containerRef = useRef<HTMLDivElement>(null);
return (
<TabsPrimitive.Root
value={activeTab}
onValueChange={setActiveTab}
className={cn("w-full space-y-12", className)}
>
<TabsPrimitive.List className="flex items-center justify-center gap-12 border-b border-white/5 pb-4">
{tabs.map((tab) => (
<TabsPrimitive.Trigger
key={tab.value}
value={tab.value}
className={cn(
"text-lg font-black uppercase tracking-tighter transition-all duration-500 outline-none relative group",
activeTab === tab.value
? "text-primary-1 scale-110"
: "text-zinc-600 hover:text-zinc-400"
)}
>
{tab.label}
{activeTab === tab.value && (
<div
className="absolute -bottom-4 left-0 w-full h-1 bg-primary-1 rounded-full"
id="active-underline"
/>
)}
</TabsPrimitive.Trigger>
))}
</TabsPrimitive.List>
<div className="relative">
{tabs.map((tab) => (
<TabsPrimitive.Content
key={tab.value}
value={tab.value}
className="outline-none data-[state=inactive]:hidden"
>
<GooeyContent>{tab.content}</GooeyContent>
</TabsPrimitive.Content>
))}
</div>
</TabsPrimitive.Root>
);
}
function GooeyContent({ children }: { children: React.ReactNode }) {
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (contentRef.current) {
const children = contentRef.current.children;
gsap.fromTo(contentRef.current,
{ x: 15, opacity: 0, scale: 0.99 },
{ x: 0, opacity: 1, scale: 1, duration: 0.4, ease: "power4.out" }
);
if (children.length > 0) {
gsap.fromTo(Array.from(children),
{ y: 10, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.3, stagger: 0.03, ease: "back.out(2)" }
);
}
}
}, []);
return (
<div
ref={contentRef}
className="min-h-[300px] flex flex-col items-center justify-center text-center space-y-6"
>
{children}
</div>
);
}