Docs
Feature Tabs

Feature Tabs

A premium, vertical tabbed interface with auto-play, 3D tilt effects, and glassmorphic styling. Perfect for showcasing high-end product features.

Platform 2.0

The future of software is here

Experience a unified platform that combines power, speed, and elegance. Built for teams that demand excellence.

Crafted for perfection

Crafted for perfection

Our design system is more than just components. It's a cohesive language that bridges the gap between design and engineering, ensuring consistency at scale with over 500+ pre-built atomic components.

Installation

Copy and paste the following code into your project.

"use client";
 
import { useState, useEffect } from "react";
import {
  motion,
  AnimatePresence,
  useMotionValue,
  useSpring,
  useTransform,
  useMotionTemplate,
} from "framer-motion";
import { cn } from "@/lib/utils";
import { LucideIcon, ArrowRight } from "lucide-react";
import Image from "next/image";
 
interface Tab {
  id: string;
  label: string;
  title: string;
  description: string;
  icon?: LucideIcon;
  image: string;
  cta?: {
    text: string;
    href?: string;
    onClick?: () => void;
  };
}
 
interface FeatureTabsProps {
  badge?: string;
  headline: string;
  description?: string;
  tabs: Tab[];
  autoPlayInterval?: number;
}
 
export function FeatureTabs({
  badge,
  headline,
  description,
  tabs,
  autoPlayInterval = 5000,
}: FeatureTabsProps) {
  const [activeTabId, setActiveTabId] = useState(tabs[0].id);
  const [isHovering, setIsHovering] = useState(false);
  const [progress, setProgress] = useState(0);
 
  const activeTab = tabs.find((tab) => tab.id === activeTabId) || tabs[0];
  const activeIndex = tabs.findIndex((tab) => tab.id === activeTabId);
 
  // Auto-play logic
  useEffect(() => {
    if (isHovering) return;
 
    const startTime = Date.now();
    const interval = setInterval(() => {
      const elapsed = Date.now() - startTime;
      const newProgress = (elapsed / autoPlayInterval) * 100;
 
      if (newProgress >= 100) {
        const nextIndex = (activeIndex + 1) % tabs.length;
        setActiveTabId(tabs[nextIndex].id);
        setProgress(0);
      } else {
        setProgress(newProgress);
      }
    }, 16); // ~60fps
 
    return () => clearInterval(interval);
  }, [activeTabId, isHovering, autoPlayInterval, activeIndex, tabs.length]);
 
  // Mouse move logic for tilt and spotlight
  const mouseX = useMotionValue(0);
  const mouseY = useMotionValue(0);
  const mouseXSpring = useSpring(mouseX, { stiffness: 500, damping: 100 });
  const mouseYSpring = useSpring(mouseY, { stiffness: 500, damping: 100 });
 
  const rotateX = useTransform(mouseYSpring, [-0.5, 0.5], ["7deg", "-7deg"]);
  const rotateY = useTransform(mouseXSpring, [-0.5, 0.5], ["-7deg", "7deg"]);
 
  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const width = rect.width;
    const height = rect.height;
 
    // Calculate percentage for rotation
    const xPct = (e.clientX - rect.left) / width - 0.5;
    const yPct = (e.clientY - rect.top) / height - 0.5;
 
    mouseX.set(xPct);
    mouseY.set(yPct);
  };
 
  const handleMouseLeave = () => {
    mouseX.set(0);
    mouseY.set(0);
    setIsHovering(false);
  };
 
  // Spotlight gradient
  const spotlightStyle = useMotionTemplate`radial-gradient(600px circle at ${useTransform(mouseX, [-0.5, 0.5], ["0%", "100%"])} ${useTransform(mouseY, [-0.5, 0.5], ["0%", "100%"])}, rgba(255,255,255,0.1), transparent 40%)`;
 
  return (
    <section className="py-24 sm:py-32 overflow-hidden bg-background text-foreground">
      <div className="container mx-auto px-4 sm:px-6">
        {/* Header */}
        <div className="max-w-3xl mx-auto text-center mb-20 space-y-6">
          {badge && (
            <motion.div
              initial={{ opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-sm font-medium"
            >
              <span className="relative flex h-2 w-2">
                <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
                <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
              </span>
              {badge}
            </motion.div>
          )}
          <motion.h2
            initial={{ opacity: 0, y: 20 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true }}
            transition={{ delay: 0.1 }}
            className="text-4xl md:text-6xl font-bold tracking-tight"
          >
            {headline}
          </motion.h2>
          {description && (
            <motion.p
              initial={{ opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ delay: 0.2 }}
              className="text-lg md:text-xl text-muted-foreground leading-relaxed max-w-2xl mx-auto"
            >
              {description}
            </motion.p>
          )}
        </div>
 
        <div className="grid lg:grid-cols-12 gap-12 lg:gap-20 items-start">
          {/* Left Column: Navigation */}
          <div className="lg:col-span-5 flex flex-col gap-4">
            {tabs.map((tab) => {
              const isActive = activeTabId === tab.id;
              return (
                <div
                  key={tab.id}
                  className="relative"
                  onMouseEnter={() => {
                    setIsHovering(true);
                    setActiveTabId(tab.id);
                    setProgress(0);
                  }}
                  onMouseLeave={() => setIsHovering(false)}
                >
                  <button
                    onClick={() => setActiveTabId(tab.id)}
                    className={cn(
                      "w-full text-left p-6 rounded-2xl transition-all duration-500 border border-transparent",
                      isActive
                        ? "bg-card/50 border-border/50 shadow-lg backdrop-blur-sm"
                        : "hover:bg-muted/30",
                    )}
                  >
                    <div className="flex items-center gap-4 mb-2">
                      {tab.icon && (
                        <div
                          className={cn(
                            "p-2 rounded-lg transition-colors duration-300",
                            isActive
                              ? "bg-primary text-primary-foreground"
                              : "bg-muted text-muted-foreground",
                          )}
                        >
                          <tab.icon className="w-5 h-5" />
                        </div>
                      )}
                      <span
                        className={cn(
                          "text-lg font-bold transition-colors duration-300",
                          isActive
                            ? "text-foreground"
                            : "text-muted-foreground",
                        )}
                      >
                        {tab.label}
                      </span>
                    </div>
 
                    <AnimatePresence>
                      {isActive && (
                        <motion.div
                          initial={{ height: 0, opacity: 0 }}
                          animate={{ height: "auto", opacity: 1 }}
                          exit={{ height: 0, opacity: 0 }}
                          transition={{ duration: 0.3 }}
                          className="overflow-hidden"
                        >
                          <p className="text-muted-foreground leading-relaxed mt-2 mb-4">
                            {tab.description}
                          </p>
                          {tab.cta && (
                            <div className="flex items-center text-primary font-medium text-sm group cursor-pointer">
                              {tab.cta.text}
                              <ArrowRight className="w-4 h-4 ml-1 transition-transform group-hover:translate-x-1" />
                            </div>
                          )}
                        </motion.div>
                      )}
                    </AnimatePresence>
                  </button>
 
                  {/* Progress Bar for Active Tab */}
                  {isActive && (
                    <div className="absolute bottom-0 left-6 right-6 h-0.5 bg-muted overflow-hidden rounded-full">
                      <motion.div
                        className="h-full bg-primary"
                        style={{ width: `${progress}%` }}
                        transition={{ duration: 0 }}
                      />
                    </div>
                  )}
                </div>
              );
            })}
          </div>
 
          {/* Right Column: Immersive Visual */}
          <div className="lg:col-span-7 relative perspective-1000">
            <motion.div
              style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
              onMouseMove={handleMouseMove}
              onMouseLeave={handleMouseLeave}
              className="relative aspect-[4/3] w-full rounded-3xl overflow-hidden border border-white/10 bg-black/50 shadow-2xl backdrop-blur-sm group"
            >
              {/* Window Controls */}
              <div className="absolute top-4 left-4 z-20 flex gap-2">
                <div className="w-3 h-3 rounded-full bg-red-500/80" />
                <div className="w-3 h-3 rounded-full bg-yellow-500/80" />
                <div className="w-3 h-3 rounded-full bg-green-500/80" />
              </div>
 
              <AnimatePresence mode="wait">
                <motion.div
                  key={activeTab.id}
                  initial={{ opacity: 0, scale: 1.1, filter: "blur(10px)" }}
                  animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
                  exit={{ opacity: 0, scale: 0.9, filter: "blur(10px)" }}
                  transition={{ duration: 0.7, ease: "circOut" }}
                  className="absolute inset-0"
                >
                  <Image
                    src={activeTab.image}
                    alt={activeTab.title}
                    fill
                    className="object-cover opacity-90"
                    priority
                  />
 
                  {/* Dynamic Gradient Overlay */}
                  <div className="absolute inset-0 bg-gradient-to-tr from-black/80 via-black/20 to-transparent" />
                </motion.div>
              </AnimatePresence>
 
              {/* Floating Glass Card for Content */}
              <motion.div
                className="absolute bottom-6 left-6 right-6 p-6 rounded-2xl bg-white/10 border border-white/10 backdrop-blur-xl shadow-lg translate-z-20"
                initial={{ opacity: 0, y: 20 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: 0.2 }}
              >
                <div className="flex items-start justify-between gap-4">
                  <div>
                    <h3 className="text-xl font-bold text-white mb-2">
                      {activeTab.title}
                    </h3>
                    <p className="text-white/70 text-sm leading-relaxed">
                      {activeTab.description}
                    </p>
                  </div>
                  {activeTab.icon && (
                    <div className="p-3 rounded-xl bg-white/10 text-white hidden sm:block">
                      <activeTab.icon className="w-6 h-6" />
                    </div>
                  )}
                </div>
              </motion.div>
 
              {/* Spotlight Effect */}
              <motion.div
                className="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-500"
                style={{
                  background: spotlightStyle,
                }}
              />
            </motion.div>
 
            {/* Background Glows */}
            <div className="absolute -inset-10 bg-gradient-to-tr from-primary/20 via-purple-500/20 to-blue-500/20 blur-3xl -z-10 opacity-50 animate-pulse" />
          </div>
        </div>
      </div>
    </section>
  );
}

Update the import paths to match your project setup.

Features

  • Vertical Auto-Play - Automatically cycles through tabs with a progress bar
  • 3D Tilt Effect - Interactive 3D tilt on the feature image container
  • Spotlight Effect - Dynamic mouse-following spotlight on the image container
  • Glassmorphism - Premium frosted glass effects for active states and cards
  • Smooth Transitions - Fluid animations for content switching and height adjustments
  • Responsive Design - Stacks gracefully on mobile, side-by-side on desktop
  • Interactive Hover - Pauses auto-play on hover for better usability
  • Customizable Speed - Adjustable auto-play interval
  • TypeScript - Full type safety

Usage

Basic Usage

import FeatureTabs from "@/components/ui/feature-tabs";
import { Zap, Shield, Rocket } from "lucide-react";
 
export default function FeaturesPage() {
  return (
    <FeatureTabs
      headline="Powerful features"
      tabs={[
        {
          id: "performance",
          label: "Performance",
          icon: Zap,
          title: "Lightning fast",
          description: "Built for speed from the ground up.",
          image: "/images/performance.jpg",
        },
        {
          id: "security",
          label: "Security",
          icon: Shield,
          title: "Secure by default",
          description: "Enterprise-grade protection for your data.",
          image: "/images/security.jpg",
        },
      ]}
    />
  );
}

Full Example with CTA and Auto-Play

import FeatureTabs from "@/components/ui/feature-tabs";
import { Zap, Shield, Rocket } from "lucide-react";
 
export default function FeaturesPage() {
  return (
    <FeatureTabs
      badge="Platform Features"
      headline="Everything you need"
      description="Explore our comprehensive suite of tools."
      autoPlayInterval={6000} // 6 seconds per tab
      tabs={[
        {
          id: "performance",
          label: "Performance",
          icon: Zap,
          title: "Lightning-fast performance",
          description:
            "Built for speed with edge computing and intelligent caching.",
          image: "/images/performance.png",
          cta: {
            text: "View Benchmarks",
            onClick: () => console.log("CTA clicked"),
          },
        },
        {
          id: "security",
          label: "Security",
          icon: Shield,
          title: "Enterprise-grade security",
          description: "Your data is protected with industry-leading security.",
          image: "/images/security.png",
          cta: {
            text: "Security Overview",
            href: "/security",
          },
        },
      ]}
    />
  );
}

Props

FeatureTabsProps

PropTypeDefaultDescription
badgestringundefinedOptional badge text above headline
headlinestringRequiredMain section headline
descriptionstringundefinedOptional description below headline
tabsTab[]RequiredArray of tab objects
autoPlayIntervalnumber5000Duration in ms for each tab

Tab Object

PropTypeDescription
idstringUnique identifier for the tab
labelstringTab label text
iconLucideIconIcon component for the tab
titlestringTitle shown in tab content
descriptionstringDescription shown in tab content
imagestringImage URL for the visual side
ctaobjectOptional call-to-action button
cta.textstringButton text
cta.hrefstringOptional link URL
cta.onClick() => voidOptional click handler

TypeScript Interface

import { LucideIcon } from "lucide-react";
 
interface Tab {
  id: string;
  label: string;
  title: string;
  description: string;
  icon?: LucideIcon;
  image: string;
  cta?: {
    text: string;
    href?: string;
    onClick?: () => void;
  };
}
 
interface FeatureTabsProps {
  badge?: string;
  headline: string;
  description?: string;
  tabs: Tab[];
  autoPlayInterval?: number;
}

Customization

Adjust Auto-Play Speed

Control how fast the tabs cycle by changing the autoPlayInterval prop (in milliseconds).

<FeatureTabs
  autoPlayInterval={3000} // Fast: 3 seconds
  // ...
/>

Disable Auto-Play

To effectively disable auto-play, set a very large interval.

<FeatureTabs
  autoPlayInterval={999999}
  // ...
/>

Customizing the 3D Tilt

The 3D tilt effect is handled by Framer Motion's useSpring and useTransform. You can adjust the stiffness, damping, and rotation range in the source code:

// In feature-tabs.tsx
const mouseXSpring = useSpring(mouseX, { stiffness: 500, damping: 100 });
const mouseYSpring = useSpring(mouseY, { stiffness: 500, damping: 100 });
 
// Adjust rotation degrees here
const rotateX = useTransform(mouseYSpring, [-0.5, 0.5], ["7deg", "-7deg"]);
const rotateY = useTransform(mouseXSpring, [-0.5, 0.5], ["-7deg", "7deg"]);

Best Practices

  1. High-Quality Images - The right column is image-heavy. Use high-resolution, visually appealing images.
  2. Concise Text - Keep descriptions short (2-3 sentences) to prevent layout shifts.
  3. Meaningful Icons - Use icons that clearly represent the tab category.
  4. Limit Tabs - 3-5 tabs is the sweet spot for this layout.
  5. Test Responsiveness - Ensure images look good on both desktop and mobile.