Kanso UI LogoKanso UI
Components/feature grid-card
cards

Feature Grid Card

A sleek hover card revealing an illuminated grid background with animated tiles and glowing grid lines.

cardgridhoveranimationtilesinteractive

Preview

Select Hover Theme Color
Products
A beautiful card component that uses shadcn/ui Card primitives under the hood with a premium hover grid.
Categories
Customize the colors to match your brand. Fully accessible and responsive with dark mode support.

🖲Usage

example-usage.tsx
import { FeatureGridCard } from "@/components/kanso/feature-grid-card"
import { LayoutGrid } from "lucide-react"

export default function FeatureGridCardDemo() {
  return (
    <div className="max-w-xs mx-auto">
      <FeatureGridCard 
        icon={<LayoutGrid className="w-full h-full" />}
        title="Products"
        description="Standard chunk of Lorem Ipsum used since the 1500s is showed below for those interested."
      />
    </div>
  )
}

Installation

1

Create folder & copy source

Create a folder named kanso inside your project's components directory (i.e. components/kanso/). Copy the source code shown in the next section, and paste it into a file named feature-grid-card.tsx inside it.

2

Add global styling

Add the custom keyframe animations and styles to your global stylesheet (e.g. app/globals.css):

globals.css
/* Add to app/globals.css */
@keyframes kanso-tile {
  0%, 12.5%, 100% { opacity: 1; }
  25%, 82.5% { opacity: 0; }
}
3

Required helper files

Ensure your project has the following helper files configured:

  • lib/utils

<>Source Code

feature-grid-card.tsx
'use client';

import * as React from 'react';
import { cn } from '@/lib/utils';
import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardContent,
} from '@/components/ui/card';

type ThemeColor = 'emerald' | 'blue' | 'violet' | 'rose' | 'amber' | 'zinc';

interface FeatureGridCardProps extends Omit<
  React.ComponentProps<typeof Card>,
  'title'
> {
  icon?: React.ReactNode;
  title: React.ReactNode;
  description: React.ReactNode;
  themeColor?: ThemeColor;
  gridLines?: boolean;
}

const themeStyles: Record<
  ThemeColor,
  {
    iconText: string;
    iconBg: string;
    iconRing: string;
    tileBg: string;
    darkGlow: string;
    lightGlow: string;
  }
> = {
  emerald: {
    iconText: 'group-hover:text-emerald-600 dark:group-hover:text-emerald-400',
    iconBg: 'group-hover:bg-emerald-500/10 dark:group-hover:bg-emerald-400/10',
    iconRing:
      'group-hover:ring-emerald-500/20 dark:group-hover:ring-emerald-400/20',
    tileBg: 'bg-emerald-500/[0.08] dark:bg-emerald-400/[0.06]',
    darkGlow:
      'conic-gradient(from 205deg at 50% 50%, rgba(16,185,129,0) 0deg, #10B981 25deg, rgba(52,211,153,0.18) 295deg, rgba(16,185,129,0) 360deg)',
    lightGlow:
      'conic-gradient(from 225deg at 50% 50%, rgba(16,185,129,0) 0deg, #10B981 25deg, #EDFAF6 285deg, #FFFFFF 345deg, rgba(16,185,129,0) 360deg)',
  },
  blue: {
    iconText: 'group-hover:text-blue-600 dark:group-hover:text-blue-400',
    iconBg: 'group-hover:bg-blue-500/10 dark:group-hover:bg-blue-400/10',
    iconRing: 'group-hover:ring-blue-500/20 dark:group-hover:ring-blue-400/20',
    tileBg: 'bg-blue-500/[0.08] dark:bg-blue-400/[0.06]',
    darkGlow:
      'conic-gradient(from 205deg at 50% 50%, rgba(59,130,246,0) 0deg, #3B82F6 25deg, rgba(96,165,250,0.18) 295deg, rgba(59,130,246,0) 360deg)',
    lightGlow:
      'conic-gradient(from 225deg at 50% 50%, rgba(59,130,246,0) 0deg, #3B82F6 25deg, #EFF6FF 285deg, #FFFFFF 345deg, rgba(59,130,246,0) 360deg)',
  },
  violet: {
    iconText: 'group-hover:text-violet-600 dark:group-hover:text-violet-400',
    iconBg: 'group-hover:bg-violet-500/10 dark:group-hover:bg-violet-400/10',
    iconRing:
      'group-hover:ring-violet-500/20 dark:group-hover:ring-violet-400/20',
    tileBg: 'bg-violet-500/[0.08] dark:bg-violet-400/[0.06]',
    darkGlow:
      'conic-gradient(from 205deg at 50% 50%, rgba(139,92,246,0) 0deg, #8B5CF6 25deg, rgba(167,139,250,0.18) 295deg, rgba(139,92,246,0) 360deg)',
    lightGlow:
      'conic-gradient(from 225deg at 50% 50%, rgba(139,92,246,0) 0deg, #8B5CF6 25deg, #F5F3FF 285deg, #FFFFFF 345deg, rgba(139,92,246,0) 360deg)',
  },
  rose: {
    iconText: 'group-hover:text-rose-600 dark:group-hover:text-rose-400',
    iconBg: 'group-hover:bg-rose-500/10 dark:group-hover:bg-rose-400/10',
    iconRing: 'group-hover:ring-rose-500/20 dark:group-hover:ring-rose-400/20',
    tileBg: 'bg-rose-500/[0.08] dark:bg-rose-400/[0.06]',
    darkGlow:
      'conic-gradient(from 205deg at 50% 50%, rgba(244,63,94,0) 0deg, #F43F5E 25deg, rgba(251,113,133,0.18) 295deg, rgba(244,63,94,0) 360deg)',
    lightGlow:
      'conic-gradient(from 225deg at 50% 50%, rgba(244,63,94,0) 0deg, #F43F5E 25deg, #FFF1F2 285deg, #FFFFFF 345deg, rgba(244,63,94,0) 360deg)',
  },
  amber: {
    iconText: 'group-hover:text-amber-600 dark:group-hover:text-amber-400',
    iconBg: 'group-hover:bg-amber-500/10 dark:group-hover:bg-amber-400/10',
    iconRing:
      'group-hover:ring-amber-500/20 dark:group-hover:ring-amber-400/20',
    tileBg: 'bg-amber-500/[0.08] dark:bg-amber-400/[0.06]',
    darkGlow:
      'conic-gradient(from 205deg at 50% 50%, rgba(245,158,11,0) 0deg, #F59E0B 25deg, rgba(251,191,36,0.18) 295deg, rgba(245,158,11,0) 360deg)',
    lightGlow:
      'conic-gradient(from 225deg at 50% 50%, rgba(245,158,11,0) 0deg, #F59E0B 25deg, #FFFBEB 285deg, #FFFFFF 345deg, rgba(245,158,11,0) 360deg)',
  },
  zinc: {
    iconText: 'group-hover:text-zinc-900 dark:group-hover:text-white',
    iconBg: 'group-hover:bg-zinc-500/10 dark:group-hover:bg-zinc-400/10',
    iconRing: 'group-hover:ring-zinc-500/20 dark:group-hover:ring-zinc-400/20',
    tileBg: 'bg-zinc-500/[0.08] dark:bg-zinc-400/[0.05]',
    darkGlow:
      'conic-gradient(from 205deg at 50% 50%, rgba(161,161,170,0) 0deg, #A1A1AA 25deg, rgba(212,212,216,0.18) 295deg, rgba(161,161,170,0) 360deg)',
    lightGlow:
      'conic-gradient(from 225deg at 50% 50%, rgba(161,161,170,0) 0deg, #A1A1AA 25deg, #F4F4F5 285deg, #FFFFFF 345deg, rgba(161,161,170,0) 360deg)',
  },
};

const tiles = [
  { top: '0', left: '0', height: '10%', width: '22.5%', delay: '0s' },
  { top: '0', left: '22.5%', height: '10%', width: '27.5%', delay: '-6s' },
  { top: '0', left: '50%', height: '10%', width: '27.5%', delay: '-4s' },
  { top: '0', left: '77.5%', height: '10%', width: '22.5%', delay: '-2s' },
  { top: '10%', left: '0', height: '22.5%', width: '22.5%', delay: '-4s' },
  { top: '10%', left: '22.5%', height: '22.5%', width: '27.5%', delay: '-2s' },
  { top: '10%', left: '50%', height: '22.5%', width: '27.5%', delay: '0s' },
  { top: '10%', left: '77.5%', height: '22.5%', width: '22.5%', delay: '-4s' },
  { top: '32.5%', left: '50%', height: '22.5%', width: '27.5%', delay: '-6s' },
  {
    top: '32.5%',
    left: '77.5%',
    height: '22.5%',
    width: '22.5%',
    delay: '-2s',
  },
];

function FeatureGridCard({
  icon,
  title,
  description,
  themeColor = 'emerald',
  gridLines = true,
  className,
  ...props
}: FeatureGridCardProps) {
  const t = themeStyles[themeColor];

  return (
    <Card
      className={cn(
        'group relative overflow-hidden transition-all duration-300 cursor-pointer border-zinc-200/50 dark:border-white/10',
        'hover:shadow-[0_8px_30px_rgb(0,0,0,0.08)] dark:hover:shadow-[0_8px_30px_rgb(0,0,0,0.2)] hover:border-zinc-300 dark:hover:border-white/20',
        className
      )}
      {...props}
    >
      {/* Background Subtle Gradient overlay */}
      <div className="pointer-events-none absolute inset-0 z-0 bg-gradient-to-b from-transparent to-zinc-50/50 dark:to-[rgba(255,255,255,0.02)]" />

      {/* Grid and Animation Layer */}
      <div
        className="pointer-events-none absolute inset-0 z-0 overflow-hidden"
        style={{
          maskImage:
            'radial-gradient(circle at 60% 0%, black 0%, black 15%, transparent 60%)',
          WebkitMaskImage:
            'radial-gradient(circle at 60% 0%, black 0%, black 15%, transparent 60%)',
        }}
      >
        {/* Animated Tiles Container */}
        <div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
          {tiles.map((tile, i) => (
            <div
              key={i}
              className={cn(
                'absolute opacity-0 group-hover:animate-[kanso-tile_8s_infinite]',
                t.tileBg
              )}
              style={{
                top: tile.top,
                left: tile.left,
                width: tile.width,
                height: tile.height,
                animationDelay: tile.delay,
              }}
            />
          ))}
        </div>

        {/* Animated Grid Lines */}
        {gridLines && (
          <div className="absolute inset-0 opacity-0 transition-opacity duration-500 group-hover:opacity-100">
            {/* Horizontal Lines */}
            {[10, 32.5, 55].map((top, i) => (
              <div
                key={`h-${i}`}
                className="absolute left-0 right-0 h-[1px] origin-left scale-x-0 bg-black/[0.04] transition-transform duration-500 group-hover:scale-x-100 dark:bg-white/[0.04]"
                style={{ top: `${top}%`, transitionDelay: `${i * 150}ms` }}
              />
            ))}
            {/* Vertical Lines */}
            {[22.5, 50, 77.5].map((left, i) => (
              <div
                key={`v-${i}`}
                className="absolute bottom-0 top-0 w-[1px] origin-top scale-y-0 bg-black/[0.04] transition-transform duration-500 group-hover:scale-y-100 dark:bg-white/[0.04]"
                style={{ left: `${left}%`, transitionDelay: `${i * 150}ms` }}
              />
            ))}
          </div>
        )}
      </div>

      {/* Shine Highlight effect (hover) */}
      <div className="pointer-events-none absolute inset-0 z-0 overflow-hidden opacity-0 transition-opacity duration-700 group-hover:opacity-100">
        <div
          className="absolute left-1/2 bottom-[45%] w-[150%] -translate-x-1/2 rounded-full opacity-40 blur-[40px] dark:opacity-0"
          style={{ paddingBottom: '150%', backgroundImage: t.lightGlow }}
        />
        <div
          className="absolute left-1/2 bottom-[45%] w-[150%] -translate-x-1/2 rounded-full opacity-0 blur-[40px] dark:opacity-25"
          style={{ paddingBottom: '150%', backgroundImage: t.darkGlow }}
        />
      </div>

      {/* Content Area */}
      <CardHeader className="relative z-10 pt-10 pb-4">
        {icon && (
          <div className="mb-4 table">
            <div className="relative flex size-10 items-center justify-center rounded-xl bg-zinc-100/50 dark:bg-zinc-800/50 ring-1 ring-zinc-200 dark:ring-zinc-700 backdrop-blur-md transition-all duration-300 shadow-sm">
              {/* Dynamic hover ring */}
              <div
                className={cn(
                  'absolute inset-0 rounded-xl bg-transparent ring-1 ring-transparent transition-colors duration-300',
                  t.iconBg,
                  t.iconRing
                )}
              />
              <div
                className={cn(
                  'relative z-10 size-5 text-zinc-600 dark:text-zinc-400 transition-colors duration-300',
                  t.iconText
                )}
              >
                {icon}
              </div>
            </div>
          </div>
        )}
        <CardTitle className="text-base font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
          {title}
        </CardTitle>
      </CardHeader>

      <CardContent className="relative z-10 pt-0 pb-6">
        <CardDescription className="text-sm leading-relaxed text-zinc-500 dark:text-zinc-400">
          {description}
        </CardDescription>
      </CardContent>
    </Card>
  );
}

export { FeatureGridCard };
export type { FeatureGridCardProps };

Props

Command Palette

Search for a command to run...