Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions animata/section/feature-grid-hover-reveal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Brain, Database, Lock, Rocket } from "lucide-react";
import FeatureGridHoverReveal, {
type FeatureGridProps,
} from "@/animata/section/feature-grid-hover-reveal";

const meta = {
title: "Section/Feature Grid Hover Reveal",
component: FeatureGridHoverReveal,
parameters: {
layout: "fullscreen",
},
tags: ["autodocs"],
argTypes: {
columns: {
control: "select",
options: [2, 3],
description: "Number of columns in the grid",
},
title: {
control: "text",
description: "Section heading",
},
subtitle: {
control: "text",
description: "Section subheading",
},
},
} satisfies Meta<typeof FeatureGridHoverReveal>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {},
render: (args: FeatureGridProps) => (
<div className="full-content w-full bg-background">
<FeatureGridHoverReveal {...args} />
</div>
),
};

export const TwoColumn: Story = {
args: {
columns: 2,
title: "Built for scale",
subtitle: "Four core capabilities that grow with your team.",
features: [
{
icon: <Rocket className="h-6 w-6" />,
title: "Instant Deploy",
description:
"Push to production in seconds with zero-downtime deployments and automatic rollbacks.",
},
{
icon: <Lock className="h-6 w-6" />,
title: "Zero-Trust Security",
description: "Every request is authenticated and authorised. No implicit trust, ever.",
},
{
icon: <Database className="h-6 w-6" />,
title: "Managed Database",
description:
"Fully managed Postgres with automatic backups, point-in-time recovery, and read replicas.",
},
{
icon: <Brain className="h-6 w-6" />,
title: "AI-Powered Insights",
description:
"Surface anomalies and opportunities automatically using built-in machine learning.",
},
],
},
render: (args: FeatureGridProps) => (
<div className="full-content w-full bg-background">
<FeatureGridHoverReveal {...args} />
</div>
),
};

export const CustomContent: Story = {
args: {
columns: 3,
title: "Why teams choose us",
subtitle: "Purpose-built features that eliminate complexity and ship value faster.",
features: [
{
icon: <Rocket className="h-6 w-6" />,
title: "One-Click Setup",
description:
"Go from sign-up to production in under five minutes with our guided onboarding.",
},
{
icon: <Brain className="h-6 w-6" />,
title: "Smart Automation",
description:
"Let AI handle the repetitive work so your team can focus on what actually matters.",
},
{
icon: <Database className="h-6 w-6" />,
title: "Reliable Storage",
description:
"99.999% uptime SLA with geo-redundant storage across three availability zones.",
},
],
},
render: (args: FeatureGridProps) => (
<div className="full-content w-full bg-background">
<FeatureGridHoverReveal {...args} />
</div>
),
};
162 changes: 162 additions & 0 deletions animata/section/feature-grid-hover-reveal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"use client";

import { BarChart, Code, Globe, Layers, Shield, Zap } from "lucide-react";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";

export interface FeatureItem {
icon: React.ReactNode;
title: string;
description: string;
}

export interface FeatureGridProps {
features?: FeatureItem[];
columns?: 2 | 3;
title?: string;
subtitle?: string;
}

const defaultFeatures: FeatureItem[] = [
{
icon: <Zap className="h-6 w-6" />,
title: "Lightning Fast",
description:
"Optimised for speed at every layer. Sub-second load times keep your users engaged and your conversion rate high.",
},
{
icon: <Shield className="h-6 w-6" />,
title: "Enterprise Security",
description:
"SOC 2 Type II certified with end-to-end encryption, SSO, and role-based access controls built in from day one.",
},
{
icon: <BarChart className="h-6 w-6" />,
title: "Advanced Analytics",
description:
"Real-time dashboards and custom reports give your team the clarity to make confident, data-driven decisions.",
},
{
icon: <Globe className="h-6 w-6" />,
title: "Global CDN",
description:
"Content delivered from 200+ edge locations worldwide so every user gets a fast, reliable experience.",
},
{
icon: <Code className="h-6 w-6" />,
title: "Developer First",
description:
"Powerful REST and GraphQL APIs, webhooks, and an SDK for every major language. Automate anything.",
},
{
icon: <Layers className="h-6 w-6" />,
title: "Seamless Integrations",
description:
"Connect to 200+ tools including Slack, Salesforce, and Stripe in minutes — no custom code required.",
},
];

const columnClass: Record<2 | 3, string> = {
2: "md:grid-cols-2",
3: "md:grid-cols-2 lg:grid-cols-3",
};

export default function FeatureGridHoverReveal({
features = defaultFeatures,
columns = 3,
title = "Everything you need to ship faster",
subtitle = "A complete platform built for modern teams — from zero to production without the growing pains.",
}: FeatureGridProps) {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);

useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
setPrefersReducedMotion(mq.matches);
const handler = (e: MediaQueryListEvent) => setPrefersReducedMotion(e.matches);
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, []);

const safeFeatures = Array.isArray(features) ? features : defaultFeatures;

return (
<section className="w-full px-4 py-16 sm:py-20 lg:py-24">
<div className="mx-auto max-w-6xl">
{(title || subtitle) && (
<div className="mb-12 text-center">
{title && (
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
{title}
</h2>
)}
{subtitle && (
<p className="mt-2 text-base text-muted-foreground sm:text-lg">{subtitle}</p>
)}
</div>
)}

<div className={cn("grid grid-cols-1 gap-6", columnClass[columns])}>
{safeFeatures.map((feature, index) => (
<FeatureCard
key={index}
feature={feature}
prefersReducedMotion={prefersReducedMotion}
/>
))}
</div>
</div>
</section>
);
}

interface FeatureCardProps {
feature: FeatureItem;
prefersReducedMotion: boolean;
}

function FeatureCard({ feature, prefersReducedMotion }: FeatureCardProps) {
const [hovered, setHovered] = useState(false);

return (
<article
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setHovered(true)}
onBlur={() => setHovered(false)}
Comment on lines +121 to +125
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the cards actually keyboard-focusable.

onFocus/onBlur never fire on a plain <article>, so keyboard users can't trigger the reveal. Add tabIndex={0} or switch to a genuinely interactive element if keyboard reveal is required.

♻️ Suggested fix
     <article
+      tabIndex={0}
       onMouseEnter={() => setHovered(true)}
       onMouseLeave={() => setHovered(false)}
       onFocus={() => setHovered(true)}
       onBlur={() => setHovered(false)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<article
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setHovered(true)}
onBlur={() => setHovered(false)}
<article
tabIndex={0}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setHovered(true)}
onBlur={() => setHovered(false)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@animata/section/feature-grid-hover-reveal.tsx` around lines 121 - 125, The
<article> element that uses onMouseEnter/onMouseLeave/onFocus/onBlur to toggle
setHovered is not keyboard-focusable; update the element in
feature-grid-hover-reveal.tsx (where setHovered is used) to be focusable by
adding tabIndex={0} or replace it with an appropriate interactive element (e.g.,
a button or anchor) and preserve the existing onFocus/onBlur handlers; if you
replace it, ensure you keep the setHovered calls and any necessary ARIA/role
attributes to retain accessibility and behavior.

className={cn(
Comment on lines +121 to +126
"group relative flex flex-col gap-4 rounded-2xl border p-6",
"bg-card text-card-foreground",
"shadow-sm transition-shadow duration-300",
!prefersReducedMotion && "transition-transform duration-300",
!prefersReducedMotion && hovered && "-translate-y-1.5 shadow-md",
hovered ? "border-primary/40" : "border-border",
)}
>
<div
className={cn(
"flex h-11 w-11 items-center justify-center rounded-xl",
"bg-primary/10 text-primary",
"transition-transform duration-200",
!prefersReducedMotion && hovered && "scale-115",
)}
Comment on lines +136 to +141
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether Tailwind extends scale with 1.15 or already uses this utility elsewhere.
fd -a 'tailwind.config.*' .
rg -n --hidden --glob 'tailwind.config.*' 'scale.*115|extend.*scale|safelist' .
rg -n --hidden 'scale-115|scale-\[1\.15\]' animata content .

Repository: codse/animata

Length of output: 39


🏁 Script executed:

# Search for Tailwind config files with different naming patterns
find . -maxdepth 2 -type f \( -name "tailwind.config.*" -o -name "tailwind.*" \) 2>/dev/null | head -20

Repository: codse/animata

Length of output: 39


🏁 Script executed:

# Check if there's a package.json and what Tailwind version is used
cat package.json | grep -A 5 -B 5 tailwind

Repository: codse/animata

Length of output: 1486


🏁 Script executed:

# Search for scale-115 usage throughout the entire codebase
rg -i 'scale-115|scale-\[1\.15\]' --type tsx --type ts --type js --type json

Repository: codse/animata

Length of output: 85


🏁 Script executed:

# Look for any Tailwind theme extensions or safelist configurations
rg -i 'extend.*scale|scale.*extend|theme.*scale' --type tsx --type ts --type js --type json

Repository: codse/animata

Length of output: 85


🏁 Script executed:

# Check the specific file mentioned in the review
cat -n animata/section/feature-grid-hover-reveal.tsx | sed -n '130,150p'

Repository: codse/animata

Length of output: 932


🏁 Script executed:

# Search for tailwind config with various extensions
find . -maxdepth 3 -name "tailwind.config*" -o -name "tailwind.*"

Repository: codse/animata

Length of output: 39


🏁 Script executed:

# Also check for postcss config which might reference tailwind
find . -maxdepth 2 \( -name "postcss.config*" -o -name ".tailwindrc*" \)

Repository: codse/animata

Length of output: 78


🏁 Script executed:

# Search for scale extensions in the codebase using rg with correct syntax
rg 'scale.*1\.15|extend.*scale' --max-count 20

Repository: codse/animata

Length of output: 186


🏁 Script executed:

# Check if scale-115 appears anywhere in the codebase (configuration or usage)
rg 'scale-115' --max-count 20

Repository: codse/animata

Length of output: 261


🏁 Script executed:

# Look at the root directory structure to understand config location
ls -la | head -20

Repository: codse/animata

Length of output: 1249


🏁 Script executed:

cat postcss.config.mjs

Repository: codse/animata

Length of output: 203


🏁 Script executed:

# Search for any Tailwind configuration inline or in other files
rg 'theme|extend|scale' postcss.config.mjs tailwind.config.* app.css globals.css 2>/dev/null | head -40

Repository: codse/animata

Length of output: 39


🏁 Script executed:

# Check if there's a theme configuration in CSS or config files
find . -maxdepth 3 -type f \( -name "*.css" -o -name "globals*" \) | head -10

Repository: codse/animata

Length of output: 151


🏁 Script executed:

# Look for any theme or extend configurations
rg 'theme.*{' --max-count 10

Repository: codse/animata

Length of output: 137


🏁 Script executed:

cat styles/globals.css

Repository: codse/animata

Length of output: 13019


🏁 Script executed:

# Search for `@theme`, `@layer`, or theme extensions in CSS files
rg '@theme|@layer|theme\s*\{' --type css

Repository: codse/animata

Length of output: 222


🏁 Script executed:

# Look for any Tailwind configuration in app or lib directories
find . -maxdepth 3 \( -name "app.tsx" -o -name "app.ts" -o -name "layout.tsx" -o -name "config.ts" \) | head -10

Repository: codse/animata

Length of output: 98


🏁 Script executed:

# Search for 115 or 1.15 scale definitions anywhere
rg '115|1\.15' --type css --type ts --type tsx --max-count 20

Repository: codse/animata

Length of output: 85


Use scale-[1.15] instead of the non-existent scale-115 utility.

scale-115 is not a default Tailwind class and is not defined in this project's theme configuration. Use the arbitrary value syntax scale-[1.15] to apply the 1.15 scale transform on hover.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@animata/section/feature-grid-hover-reveal.tsx` around lines 136 - 141, The
Tailwind class "scale-115" in the className expression inside the FeatureGrid
hover component is invalid; replace that token with the arbitrary value class
"scale-[1.15]" wherever it's conditionally appended (the className block that
uses cn(...) with the prefersReducedMotion and hovered variables in
feature-grid-hover-reveal.tsx) so the 1.15 transform is applied correctly when
hovered and reduced-motion is not requested.

Comment on lines +127 to +141
>
{feature.icon}
</div>

<div>
<h3 className="text-lg font-semibold text-foreground">{feature.title}</h3>
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">{feature.description}</p>
</div>

{!prefersReducedMotion && (
<span
className={cn(
"pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-primary/20 transition-opacity duration-300",
hovered ? "opacity-100" : "opacity-0",
)}
aria-hidden="true"
/>
)}
</article>
);
}
71 changes: 71 additions & 0 deletions content/docs/section/feature-grid-hover-reveal.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Feature Grid Hover Reveal
description: A responsive SaaS feature section with icon cards, hover lift, border highlight, and icon scale micro-interactions.
author: KeenIsHere
published: true
---

<ComponentPreview name="section-feature-grid-hover-reveal--primary" />

## Overview

Feature Grid Hover Reveal is a polished section component for SaaS landing pages. It displays a grid of feature cards with icon, title, and description. On hover, cards lift, the border highlights, and the icon scales up using only transform and opacity. Animations are skipped when prefers-reduced-motion is enabled.

## Features

Comment on lines +8 to +15
- Responsive grid: 1 column mobile, 2 tablet, 3 desktop
- Hover lift: -translate-y-1.5 with 300ms transition
- Icon scale: scale-115 with 200ms transition
- Border highlight: border-primary/40 on hover
- Ring overlay: inset ring fades in via opacity
- prefers-reduced-motion: all animations off when set
- Dark mode: full Tailwind token support
- Configurable: 2 or 3 column layout via prop
- Zero-props render: 6 default SaaS feature items built in

## Usage

```tsx
import FeatureGridHoverReveal from "@/animata/section/feature-grid-hover-reveal";

export default function Page() {
return <FeatureGridHoverReveal />;
}
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| features | FeatureItem[] | 6 built-in items | Array of feature cards |
| columns | 2 or 3 | 3 | Grid column count |
| title | string | Built-in heading | Section title |
| subtitle | string | Built-in subtitle | Section subtitle |

## Types

```typescript
interface FeatureItem {
icon: React.ReactNode;
title: string;
description: string;
}

interface FeatureGridProps {
features?: FeatureItem[];
columns?: 2 | 3;
title?: string;
subtitle?: string;
}
```

## Accessibility

- Semantic section and article elements
- Hover state triggered on focus and blur for keyboard access
- Decorative ring span has aria-hidden set to true
- Icon container meets 44x44px minimum touch target
- All motion respects prefers-reduced-motion
## Credits

Built by [Keen Sha](https://github.com/KeenIsHere).
Loading