Docs
Feature Cards with Hover

Feature Cards with Hover

Interactive feature cards that reveal additional content on hover. Perfect for showcasing product features with benefits, stats, and CTAs.

Everything you need to succeed

Powerful features designed to help your business grow

Lightning Fast

Experience blazing-fast performance with our optimized infrastructure and global CDN network.

Enterprise Security

Bank-level security with end-to-end encryption, compliance certifications, and advanced threat protection.

Advanced Analytics

Get deep insights with real-time analytics, custom dashboards, and predictive intelligence.

Team Collaboration

Work together seamlessly with real-time collaboration, comments, and team management tools.

Global Infrastructure

Deploy worldwide with our multi-region infrastructure and intelligent routing for optimal performance.

Mobile First

Native mobile experience with offline support, push notifications, and cross-platform SDKs.

Privacy Focused

GDPR compliant with data anonymization, user consent management, and privacy-first design.

Infinite Scale

Scale from zero to millions of users without breaking a sweat with our auto-scaling architecture.

Developer Friendly

Comprehensive REST & GraphQL APIs with SDKs, webhooks, and extensive documentation.

Want to see all features in action?

Request a Demo

Installation

Install dependencies

npm install framer-motion

Copy and paste the following code into your project.

"use client";
 
import { useState, createElement } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ArrowRight, Check } from "lucide-react";
 
interface Feature {
  id: string;
  title: string;
  description: string;
  icon: React.ElementType;
  color: string;
  benefits?: string[];
  stats?: {
    label: string;
    value: string;
  };
  ctaText?: string;
  ctaLink?: string;
  image?: string;
}
 
interface FeatureCardsHoverProps {
  title?: string;
  description?: string;
  features: Feature[];
  className?: string;
}
 
export function FeatureCardsHover({
  title = "Everything you need to succeed",
  description = "Powerful features designed to help your business grow",
  features,
  className = "",
}: FeatureCardsHoverProps) {
  const [hoveredCard, setHoveredCard] = useState<string | null>(null);
 
  return (
    <section className={`w-full bg-white py-20 dark:bg-black ${className}`}>
      <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
        <div className="mb-16 text-center">
          <h2 className="mb-4 text-4xl font-bold text-zinc-900 dark:text-white sm:text-5xl">
            {title}
          </h2>
          <p className="mx-auto max-w-2xl text-lg text-zinc-600 dark:text-zinc-400">
            {description}
          </p>
        </div>
 
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
          {features.map((feature, index) => {
            const Icon = feature.icon;
            const isHovered = hoveredCard === feature.id;
 
            return (
              <motion.div
                key={feature.id}
                initial={{ opacity: 0, y: 20 }}
                animate={{ opacity: 1, y: 0 }}
                transition={{ delay: index * 0.1 }}
                onMouseEnter={() => setHoveredCard(feature.id)}
                onMouseLeave={() => setHoveredCard(null)}
                className="group relative overflow-hidden rounded-2xl border-2 border-zinc-200 bg-white transition-all duration-300 hover:shadow-lg dark:border-zinc-800 dark:bg-zinc-950"
                style={
                  isHovered
                    ? {
                        borderColor: feature.color,
                        boxShadow: `0 0 0 1px ${feature.color}20`,
                      }
                    : {}
                }
              >
                <div
                  className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-500 group-hover:opacity-20"
                  style={{
                    background: `radial-gradient(circle at 50% 0%, ${feature.color}, transparent 70%)`,
                  }}
                />
 
                <div className="relative h-full p-8">
                  <div className="mb-6">
                    <motion.div
                      className={`mb-6 inline-flex h-14 w-14 items-center justify-center rounded-xl transition-all duration-300 ${
                        !isHovered ? "bg-zinc-100 dark:bg-zinc-900" : ""
                      }`}
                      style={
                        isHovered
                          ? {
                              backgroundColor: feature.color,
                              boxShadow: `0 0 30px ${feature.color}40`,
                            }
                          : {}
                      }
                      animate={{
                        scale: isHovered ? 1.1 : 1,
                        rotate: isHovered ? 5 : 0,
                      }}
                      transition={{ duration: 0.3, ease: "easeOut" }}
                    >
                      {createElement(Icon, {
                        className: isHovered
                          ? "h-7 w-7 text-white transition-colors duration-300"
                          : "h-7 w-7 text-zinc-600 transition-colors duration-300 dark:text-zinc-400",
                      })}
                    </motion.div>
 
                    <h3 className="mb-3 text-xl font-bold text-zinc-900 dark:text-white">
                      {feature.title}
                    </h3>
 
                    <p className="leading-relaxed text-zinc-600 dark:text-zinc-400">
                      {feature.description}
                    </p>
                  </div>
 
                  <AnimatePresence initial={false}>
                    {isHovered && (
                      <motion.div
                        initial={{ opacity: 0, height: 0 }}
                        animate={{ opacity: 1, height: "auto" }}
                        exit={{ opacity: 0, height: 0 }}
                        transition={{ duration: 0.3, ease: "easeInOut" }}
                        className="overflow-hidden"
                      >
                        {feature.benefits && feature.benefits.length > 0 && (
                          <div className="mb-6 space-y-3">
                            {feature.benefits.map((benefit, idx) => (
                              <motion.div
                                key={idx}
                                initial={{ opacity: 0, x: -10 }}
                                animate={{ opacity: 1, x: 0 }}
                                transition={{ delay: idx * 0.1 }}
                                className="flex items-start gap-3"
                              >
                                <div
                                  className="mt-1 flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full"
                                  style={{
                                    backgroundColor: `${feature.color}20`,
                                  }}
                                >
                                  <Check
                                    className="h-3 w-3"
                                    style={{ color: feature.color }}
                                  />
                                </div>
                                <span className="text-sm text-zinc-700 dark:text-zinc-300">
                                  {benefit}
                                </span>
                              </motion.div>
                            ))}
                          </div>
                        )}
 
                        {feature.stats && (
                          <motion.div
                            initial={{ opacity: 0, y: 10 }}
                            animate={{ opacity: 1, y: 0 }}
                            transition={{ delay: 0.2 }}
                            className="mb-6 rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900/50"
                          >
                            <div
                              className="text-2xl font-bold"
                              style={{ color: feature.color }}
                            >
                              {feature.stats.value}
                            </div>
                            <div className="text-sm text-zinc-600 dark:text-zinc-400">
                              {feature.stats.label}
                            </div>
                          </motion.div>
                        )}
 
                        <motion.a
                          href={feature.ctaLink || "#"}
                          initial={{ opacity: 1, y: 10 }}
                          animate={{ opacity: 1, y: 0 }}
                          transition={{ delay: 0.2 }}
                          className="group/cta flex items-center justify-between rounded-lg border border-zinc-300 bg-zinc-100 px-4 py-3 transition-all duration-300 hover:border-zinc-400 hover:bg-zinc-200 dark:border-zinc-700 dark:bg-zinc-900 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
                          style={
                            isHovered ? { borderColor: feature.color } : {}
                          }
                        >
                          <span className="font-semibold text-zinc-900 dark:text-white">
                            {feature.ctaText || "Learn More"}
                          </span>
                          <ArrowRight className="h-4 w-4 text-zinc-600 transition-transform duration-300 group-hover/cta:translate-x-1 dark:text-zinc-400" />
                        </motion.a>
                      </motion.div>
                    )}
                  </AnimatePresence>
                </div>
 
                {/* Corner Accent */}
                <div
                  className="pointer-events-none absolute right-0 top-0 h-20 w-20 opacity-0 transition-opacity duration-500 group-hover:opacity-100"
                  style={{
                    background: `radial-gradient(circle at 100% 0%, ${feature.color}40, transparent 70%)`,
                  }}
                />
              </motion.div>
            );
          })}
        </div>
 
        {/* Bottom CTA */}
        <div className="mt-16 text-center">
          <p className="mb-6 text-zinc-600 dark:text-zinc-400">
            Want to see all features in action?
          </p>
          <motion.a
            href="#demo"
            className="inline-flex items-center gap-2 rounded-lg bg-zinc-900 px-8 py-4 font-semibold text-white transition-all duration-300 hover:bg-zinc-800 dark:bg-white dark:text-black dark:hover:bg-zinc-100"
            whileHover={{ scale: 1.05 }}
            whileTap={{ scale: 0.95 }}
          >
            <span>Request a Demo</span>
            <ArrowRight className="h-5 w-5" />
          </motion.a>
        </div>
      </div>
    </section>
  );
}

Update the import paths to match your project setup.

Features

  • Hover reveal - Additional content appears smoothly on hover
  • Animated icons - Icons scale and rotate on hover
  • Benefits list - Show key benefits with checkmarks
  • Stats display - Highlight important metrics
  • CTA buttons - Call-to-action with hover effects
  • Color themes - Each card has its own color
  • Gradient effects - Background and corner accents
  • Border animations - Colored borders on hover
  • Responsive grid - 1-3 columns based on screen size
  • TypeScript support - Full type safety

Usage

Basic Usage

import FeatureCardsHover from "@/components/ui/feature-cards-hover";
import {Zap, Shield, BarChart3} from "lucide-react";
 
const features = [
  {
    id: "performance",
    title: "Lightning Fast",
    description: "Experience blazing-fast performance",
    icon: Zap,
    color: "#f59e0b",
    benefits: [
      "Sub-50ms response times",
      "Auto-scaling infrastructure",
      "Edge caching",
    ],
    stats: {
      label: "Response Time",
      value: "<50ms",
    },
    ctaText: "Learn More",
    ctaLink: "/performance",
  },
  // ... more features
];
 
export default function Page() {
  return <FeatureCardsHover features={features} />;
}

With Custom Title

<FeatureCardsHover
  title="Why Choose Us"
  description="Features that set us apart from the competition"
  features={features}
/>

Minimal Cards (No Hover Content)

const features = [
  {
    id: "simple",
    title: "Simple Feature",
    description: "Just title and description",
    icon: Zap,
    color: "#3b82f6",
    // No benefits, stats, or CTA
  },
];

Props

FeatureCardsHoverProps

PropTypeDefaultDescription
featuresFeature[]RequiredArray of feature objects
titlestring"Everything you need..."Section title
descriptionstring"Powerful features..."Section description
classNamestring""Additional CSS classes

Feature

PropTypeRequiredDescription
idstringYesUnique identifier
titlestringYesFeature title
descriptionstringYesFeature description
iconReact.ElementTypeYesLucide icon component
colorstringYesHex color code
benefitsstring[]NoList of benefits (shown on hover)
statsStatNoStat object (shown on hover)
ctaTextstringNoCTA button text
ctaLinkstringNoCTA button link
imagestringNoFeature image URL

Stat

PropTypeRequiredDescription
labelstringYesStat label
valuestringYesStat value

TypeScript Interface

interface Feature {
  id: string;
  title: string;
  description: string;
  icon: React.ElementType;
  color: string;
  benefits?: string[];
  stats?: {
    label: string;
    value: string;
  };
  ctaText?: string;
  ctaLink?: string;
  image?: string;
}
 
interface FeatureCardsHoverProps {
  title?: string;
  description?: string;
  features: Feature[];
  className?: string;
}

Use Cases

Perfect for:

  • SaaS product features
  • Service offerings
  • Platform capabilities
  • Tool highlights
  • Benefit showcases
  • Feature comparisons
  • Product tours
  • Landing pages

Customization

Change Hover Animation Speed

// In the AnimatePresence section
<motion.div
  initial={{ opacity: 0, height: 0 }}
  animate={{ opacity: 1, height: "auto" }}
  exit={{ opacity: 0, height: 0 }}
  transition={{ duration: 0.5 }} // Change from 0.3 to 0.5
>

Always Show Benefits (No Hover)

// Remove AnimatePresence and conditional rendering
<div className="mb-6 space-y-3">
  {feature.benefits?.map((benefit, idx) => (
    // ... benefit content
  ))}
</div>

Add Feature Images

// Add before the icon section
{
  feature.image && (
    <img
      src={feature.image}
      alt={feature.title}
      className="mb-4 h-40 w-full rounded-lg object-cover"
    />
  );
}

Change Grid Columns

// Modify the grid classes
className = "grid gap-6 md:grid-cols-2 xl:grid-cols-4"; // 4 columns on xl

Remove Stats

// Simply don't include stats property in feature objects
const features = [
  {
    id: "feature",
    title: "Feature",
    description: "Description",
    icon: Zap,
    color: "#3b82f6",
    benefits: ["Benefit 1", "Benefit 2"],
    // No stats property
  },
];

Custom Hover Trigger

// Change hover trigger to click
onClick={() => setHoveredCard(hoveredCard === feature.id ? null : feature.id)}
// Remove onHoverStart and onHoverEnd

Accessibility

  • Keyboard navigation - Cards are focusable
  • Screen readers - Semantic HTML structure
  • Color contrast - WCAG AA compliant
  • Motion - Respects prefers-reduced-motion
  • Focus states - Visible focus indicators

Performance

  • Lazy animations - Only animates visible cards
  • GPU acceleration - Uses transform and opacity
  • Optimized re-renders - Minimal state updates
  • Efficient hover - Single state for all cards